Compare commits

...

101 Commits

Author SHA1 Message Date
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
DLmaster361
9a0e7265c6 feat(core): 语音功能上线 2025-06-01 03:16:56 +08:00
DLmaster361
3f8e2fbe6b Merge branch 'dev' 2025-05-31 13:37:06 +08:00
DLmaster361
590b13e916 ci: 移除测试打包流程 2025-05-31 13:36:09 +08:00
DLmaster361
0f6aee56e5 ci: 修正测试工作流名称 2025-05-31 11:21:13 +08:00
DLmaster361
daf18e7295 Merge branch 'dev' 2025-05-31 11:20:04 +08:00
DLmaster361
9bcc87f663 ci: 添加引入打包action的测试工作流 2025-05-31 11:19:36 +08:00
DLmaster361
e7205ce0aa fix(services): 非UI组件转为QObject类 2025-05-30 21:06:39 +08:00
DLmaster361
e3c4b2edc8 fix(core): 网络模块支持并发请求 2025-05-30 20:30:23 +08:00
DLmaster361
222a3b35a2 fix(maa): 修复ADB与模拟器相关日志信息报错时不显示 2025-05-29 21:06:09 +08:00
DLmaster361
cd5dfd56b2 Merge branch 'dev' 2025-05-28 23:13:15 +08:00
DLmaster361
7d5c6b8222 docs: 添加DeepWiki徽章 2025-05-28 23:12:56 +08:00
DLmaster361
4dbf4736e4 Merge branch 'dev' 2025-05-28 21:18:24 +08:00
DLmaster361
d50504181e feat(ui): 吐司通知在主窗口隐藏时不再弹出 2025-05-28 21:17:14 +08:00
DLmaster361
c7e94dfcd1 Merge branch 'DLMS_dev' into dev 2025-05-27 18:17:29 +08:00
DLmaster361
a752b67ca1 feat(ui): UI界面添加自动日常代理任务序列设置项 2025-05-26 16:05:46 +08:00
DLmaster361
078736337d fix: 修复雷电模拟器静默模式无法正常识别模拟器是否隐藏相关问题 2025-05-25 23:31:37 +08:00
DLmaster361
de1058a28c feat(core): 计划表功能上线 2025-05-25 21:12:22 +08:00
DLmaster361
740797a689 Merge branch 'dev' into DLMS_dev 2025-05-24 19:56:16 +08:00
DLmaster361
26328920a2 chore(ui): 输入文本框适配文本插入操作 2025-05-24 19:55:49 +08:00
DLmaster361
9c447bbdf9 Merge branch 'ClozyA_dev' into dev 2025-05-24 18:34:03 +08:00
DLmaster361
fac85a889f fix(ui): 简单优化用户通知显示效果 2025-05-24 18:33:46 +08:00
DLmaster361
f5d898c89e fix(maa): 将通知判定过程全部移入push_notification 2025-05-23 22:59:09 +08:00
974a4b634a feat(ui): 优化敏感信息显示逻辑 2025-05-23 15:19:35 +08:00
3127c83603 feat(notification): 增加用户单独通知功能 2025-05-23 15:01:26 +08:00
DLmaster361
8d69e43f72 feat(ui): 初步添加周计划表 2025-05-22 23:20:38 +08:00
DLmaster361
86df9e7a50 chore(ui): 优化二级菜单显示效果 2025-05-21 01:00:38 +08:00
59ff9bf818 refactor(notification): 重构通知模块 2025-05-20 02:11:54 +08:00
DLmaster361
1641e32e3d feat(ui): 完成完整用户通知子菜单设计 2025-05-19 17:31:50 +08:00
DLmaster361
a482087abd feat(ui): 初步完成用户通知相关界面 2025-05-19 16:54:14 +08:00
bc5b15cec2 feat(notification): 优化用户通知服务 2025-05-19 01:10:20 +08:00
3787c25a77 Merge branch 'dev' into ClozyA_dev 2025-05-19 00:18:37 +08:00
DLmaster361
0b06b499e4 fix(models): 修复雷电ADB端口号相关问题 2025-05-18 23:35:35 +08:00
DLmaster361
04079dd57b Merge branch 'dev' of github.com:DLmaster361/AUTO_MAA into dev 2025-05-18 13:54:19 +08:00
DLmaster361
34ac0c5ab3 fix: 主动关闭剩余理智选项 2025-05-18 13:54:14 +08:00
0d904b229e feat(core): 初步完成新增用户单独通知功能 2025-05-18 01:57:48 +08:00
c0f887ff9d refactor(notification): 更新任务报告标题格式,增加日期前缀 2025-05-18 00:17:06 +08:00
DLmaster361
cf95075d01 fix(ui): 修复窗口最大化功能异常 2025-05-17 22:26:41 +08:00
DLmaster361
d78a764d87 fix: 修正搜索ADB时的等待方法 2025-05-17 00:07:34 +08:00
DLmaster
a368f4b722 feat(ui): 用户仪表盘支持直接控制用户状态
优化仪表盘排版
2025-05-16 23:27:53 +08:00
DLmaster361
803fe4568f chore(ui): 优化实现方法 2025-05-16 23:23:02 +08:00
DLmaster361
1162d5dcc1 feat(ui): 添加对剩余天数为0的展示 2025-05-16 22:50:54 +08:00
Zrief
aa83058e39 Update member_manager.py 2025-05-16 21:56:20 +08:00
Zrief
061f205224 Merge branch 'DLmaster361:dev' into dev 2025-05-16 21:55:16 +08:00
Zrief
5d966f98df 修改逻辑不完备的地方 2025-05-16 21:53:03 +08:00
DLmaster361
0037914db8 Merge branch 'DLMS_dev' into dev 2025-05-16 20:41:04 +08:00
DLmaster361
13d0115475 feat(core): 添加ADB端口号宽幅适配能力 2025-05-16 20:09:29 +08:00
Zrief
5bdb5ad2bf 优化脚本仪表盘排版
可以直接在仪表盘里面开关用户状态了
2025-05-15 21:20:30 +08:00
a5d733c8df feat(notification): 为自动代理统计报告添加日期前缀 2025-05-15 16:39:56 +08:00
DLmaster361
0b038e2997 Merge branch 'main' into dev 2025-05-12 14:54:17 +08:00
DLmaster361
5e46040db6 feat: 日志分析忽略MAA超时提示 2025-05-12 14:53:57 +08:00
DLmaster361
f2b04dd0f6 Merge branch 'dev' 2025-05-11 23:28:18 +08:00
DLmaster361
2177c1b40e fix(models): 适配MAAv5.16.3的ADB报错信息更改 2025-05-11 23:26:42 +08:00
DLmaster361
d1f4cffe8f feat(ui): 主调度台添加仅一次电源任务,UI样式优化 2025-05-10 23:15:54 +08:00
DLmaster361
74ce441b90 feat(core): 电源相关选项改为所有任务完成后生效 2025-05-09 21:52:18 +08:00
DLmaster361
5893aa2426 feat(ui): 自定义基建显示配置名称 2025-05-09 19:54:07 +08:00
DLmaster361
cb7e7bf9d4 Merge branch 'dev' 2025-05-09 14:54:29 +08:00
DLmaster361
fbfdc6aa12 feat: 优化更新方式 2025-05-09 14:54:09 +08:00
80 changed files with 4655 additions and 1916 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,78 +42,198 @@ 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
- 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
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
$UPDATER_VERSION=(Get-Content -Path "version_info.txt" -TotalCount 2 | Select-Object -Index 1).Trim()
"updater_version=$UPDATER_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
- name: Compile Inno Setup Script
run: |
choco install innosetup
echo "C:\Program Files (x86)\Inno Setup 6" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
Expand-Archive -Path AUTO_MAA_${{ env.AUTO_MAA_version }}.zip -DestinationPath AUTO_MAA -Force
ISCC.exe "AUTO_MAA.iss"
- name: Upload Setup Artifact
uses: actions/upload-artifact@v4
with:
name: AUTO_MAA_Setup
path: AUTO_MAA-Setup.exe
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: |
@@ -120,8 +242,23 @@ jobs:
NAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
TAGNAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
NOTES_MAIN="$(sed 's/\r$//g' <(tail -n +3 version_info.txt))"
NOTES_TAIL="\`\`\`本release通过GitHub Actions自动构建\`\`\`"
NOTES="$NOTES_MAIN<br><br>$NOTES_TAIL"
NOTES="$NOTES_MAIN
## 代码签名策略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
PRERELEASE_FLAG=""
else
@@ -130,6 +267,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

View File

@@ -13,7 +13,8 @@
<a href="https://github.com/DLmaster361/AUTO_MAA/issues"><img alt="GitHub Issues" src="https://img.shields.io/github/issues/DLmaster361/AUTO_MAA?style=flat-square"></a>
<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://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://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&source=auto_maa-readme"><img alt="mirrorc" src="https://img.shields.io/badge/Mirror%E9%85%B1-%239af3f6?logo=countingworkspro&logoColor=4f46e5"></a>
</p>
## 软件介绍
@@ -22,6 +23,11 @@
本软件是明日方舟第三方软件`MAA`的第三方工具即第3<sup>3</sup>方软件。旨在优化MAA多账号功能体验并通过一些方法解决MAA项目未能解决的部分问题提高代理的稳定性。
- **集中管理**一站式管理多个MAA脚本与多个用户配置和凌乱的散装脚本窗口说再见
- **无人值守**自动处理MAA相关报错再也不用为代理任务卡死时自己不在电脑旁烦恼啦
- **配置灵活**:通过调度队列与脚本的组合,自由实现您能想到的所有调度需求!
- **信息统计**:自动统计用户的公招与关卡掉落物,看看这个月您的收益是多少!
### 原理
本软件可以存储多个明日方舟账号数据,并通过以下流程实现代理功能:
@@ -32,11 +38,9 @@
### 优势
- **节省运行开销:** 只需要一份MAA软件与一个模拟器无需多开就能完成多账号代理羸弱的电脑也能代理日常
- **自定义空间大:** 依靠高级用户配置模式支持MAA几乎所有设置选项自定义支持模拟器多开
- **调度方法自由:** 通过调度队列功能自由实现MAA多开等多种调度方式
- **一键代理无忧:** 无须中途手动修改MAA配置将繁琐交给AUTO_MAA把游戏留给自己。
- **代理结果复核:** 通过人工排查功能核实各用户代理情况,堵住自动代理的最后一丝风险。
- **高效稳定**:通过日志监测、异常处理等机制,保障代理任务顺利完成
- **简洁易用**:无需手动修改配置文件,实现自动化调度与多开管理
- **兼容扩展**:支持 MAA 几乎所有的配置选项,满足不同用户需求
## 重要声明
@@ -48,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信任区以及防病毒软件的信任区或开发者目录避免被误杀。
---
# 使用方法
@@ -71,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/)。
## 贡献者
感谢以下贡献者对本项目做出的贡献
@@ -83,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

@@ -33,7 +33,6 @@ from .core import QueueConfig, MaaConfig, MaaUserConfig, Task, TaskManager, Main
from .models import MaaManager
from .services import Notify, Crypto, System
from .ui import AUTO_MAA
from .utils import DownloadManager
__all__ = [
"QueueConfig",
@@ -47,5 +46,4 @@ __all__ = [
"Crypto",
"System",
"AUTO_MAA",
"DownloadManager",
]

View File

@@ -29,9 +29,10 @@ __version__ = "4.2.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .config import QueueConfig, MaaConfig, MaaUserConfig, Config
from .config import QueueConfig, MaaConfig, MaaUserConfig, MaaPlanConfig, Config
from .main_info_bar import MainInfoBar
from .network import Network
from .sound_player import SoundPlayer
from .task_manager import Task, TaskManager
from .timer import MainTimer
@@ -40,8 +41,10 @@ __all__ = [
"QueueConfig",
"MaaConfig",
"MaaUserConfig",
"MaaPlanConfig",
"MainInfoBar",
"Network",
"SoundPlayer",
"Task",
"TaskManager",
"MainTimer",

View File

@@ -27,12 +27,14 @@ v4.3
from loguru import logger
from PySide6.QtCore import Signal
import argparse
import sqlite3
import json
import sys
import shutil
import re
import base64
import calendar
from datetime import datetime, timedelta, date
from collections import defaultdict
from pathlib import Path
@@ -78,110 +80,12 @@ class UrlListValidator(ConfigValidator):
return list(set([_ for _ in urls if self.validate(_)]))
class GlobalConfig(QConfig):
"""局配置"""
class LQConfig(QConfig):
"""配置"""
def __init__(self) -> None:
super().__init__()
self.function_HomeImageMode = OptionsConfigItem(
"Function",
"HomeImageMode",
"默认",
OptionsValidator(["默认", "自定义", "主题图像"]),
)
self.function_HistoryRetentionTime = OptionsConfigItem(
"Function",
"HistoryRetentionTime",
0,
OptionsValidator([7, 15, 30, 60, 90, 180, 365, 0]),
)
self.function_IfAllowSleep = ConfigItem(
"Function", "IfAllowSleep", False, BoolValidator()
)
self.function_IfSilence = ConfigItem(
"Function", "IfSilence", False, BoolValidator()
)
self.function_BossKey = ConfigItem("Function", "BossKey", "")
self.function_UnattendedMode = ConfigItem(
"Function", "UnattendedMode", False, BoolValidator()
)
self.function_IfAgreeBilibili = ConfigItem(
"Function", "IfAgreeBilibili", False, BoolValidator()
)
self.function_IfSkipMumuSplashAds = ConfigItem(
"Function", "IfSkipMumuSplashAds", False, BoolValidator()
)
self.start_IfSelfStart = ConfigItem(
"Start", "IfSelfStart", False, BoolValidator()
)
self.start_IfRunDirectly = ConfigItem(
"Start", "IfRunDirectly", False, BoolValidator()
)
self.start_IfMinimizeDirectly = ConfigItem(
"Start", "IfMinimizeDirectly", False, BoolValidator()
)
self.ui_IfShowTray = ConfigItem("UI", "IfShowTray", False, BoolValidator())
self.ui_IfToTray = ConfigItem("UI", "IfToTray", False, BoolValidator())
self.ui_size = ConfigItem("UI", "size", "1200x700")
self.ui_location = ConfigItem("UI", "location", "100x100")
self.ui_maximized = ConfigItem("UI", "maximized", False, BoolValidator())
self.notify_SendTaskResultTime = OptionsConfigItem(
"Notify",
"SendTaskResultTime",
"不推送",
OptionsValidator(["不推送", "任何时刻", "仅失败时"]),
)
self.notify_IfSendStatistic = ConfigItem(
"Notify", "IfSendStatistic", False, BoolValidator()
)
self.notify_IfSendSixStar = ConfigItem(
"Notify", "IfSendSixStar", False, BoolValidator()
)
self.notify_IfPushPlyer = ConfigItem(
"Notify", "IfPushPlyer", False, BoolValidator()
)
self.notify_IfSendMail = ConfigItem(
"Notify", "IfSendMail", False, BoolValidator()
)
self.notify_SMTPServerAddress = ConfigItem("Notify", "SMTPServerAddress", "")
self.notify_AuthorizationCode = ConfigItem("Notify", "AuthorizationCode", "")
self.notify_FromAddress = ConfigItem("Notify", "FromAddress", "")
self.notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
self.notify_IfServerChan = ConfigItem(
"Notify", "IfServerChan", False, BoolValidator()
)
self.notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
self.notify_ServerChanChannel = ConfigItem("Notify", "ServerChanChannel", "")
self.notify_ServerChanTag = ConfigItem("Notify", "ServerChanTag", "")
self.notify_IfCompanyWebHookBot = ConfigItem(
"Notify", "IfCompanyWebHookBot", False, BoolValidator()
)
self.notify_CompanyWebHookBotUrl = ConfigItem(
"Notify", "CompanyWebHookBotUrl", ""
)
self.notify_IfPushDeer = ConfigItem(
"Notify", "IfPushDeer", False, BoolValidator()
)
self.notify_IfPushDeerKey = ConfigItem("Notify", "PushDeerKey", "")
self.update_IfAutoUpdate = ConfigItem(
"Update", "IfAutoUpdate", False, BoolValidator()
)
self.update_UpdateType = OptionsConfigItem(
"Update", "UpdateType", "stable", OptionsValidator(["stable", "beta"])
)
self.update_ThreadNumb = RangeConfigItem(
"Update", "ThreadNumb", 8, RangeValidator(1, 32)
)
self.update_ProxyUrlList = ConfigItem(
"Update", "ProxyUrlList", [], UrlListValidator()
)
self.update_MirrorChyanCDK = ConfigItem("Update", "MirrorChyanCDK", "")
def toDict(self, serialize=True):
"""convert config items to `dict`"""
items = {}
@@ -247,7 +151,113 @@ class GlobalConfig(QConfig):
self.theme = self.get(self._cfg.themeMode)
class QueueConfig(QConfig):
class GlobalConfig(LQConfig):
"""全局配置"""
def __init__(self) -> None:
super().__init__()
self.function_HomeImageMode = OptionsConfigItem(
"Function",
"HomeImageMode",
"默认",
OptionsValidator(["默认", "自定义", "主题图像"]),
)
self.function_HistoryRetentionTime = OptionsConfigItem(
"Function",
"HistoryRetentionTime",
0,
OptionsValidator([7, 15, 30, 60, 90, 180, 365, 0]),
)
self.function_IfAllowSleep = ConfigItem(
"Function", "IfAllowSleep", False, BoolValidator()
)
self.function_IfSilence = ConfigItem(
"Function", "IfSilence", False, BoolValidator()
)
self.function_BossKey = ConfigItem("Function", "BossKey", "")
self.function_UnattendedMode = ConfigItem(
"Function", "UnattendedMode", False, BoolValidator()
)
self.function_IfAgreeBilibili = ConfigItem(
"Function", "IfAgreeBilibili", False, BoolValidator()
)
self.function_IfSkipMumuSplashAds = ConfigItem(
"Function", "IfSkipMumuSplashAds", False, BoolValidator()
)
self.voice_Enabled = ConfigItem("Voice", "Enabled", False, BoolValidator())
self.voice_Type = OptionsConfigItem(
"Voice", "Type", "simple", OptionsValidator(["simple", "noisy"])
)
self.start_IfSelfStart = ConfigItem(
"Start", "IfSelfStart", False, BoolValidator()
)
self.start_IfRunDirectly = ConfigItem(
"Start", "IfRunDirectly", False, BoolValidator()
)
self.start_IfMinimizeDirectly = ConfigItem(
"Start", "IfMinimizeDirectly", False, BoolValidator()
)
self.ui_IfShowTray = ConfigItem("UI", "IfShowTray", False, BoolValidator())
self.ui_IfToTray = ConfigItem("UI", "IfToTray", False, BoolValidator())
self.ui_size = ConfigItem("UI", "size", "1200x700")
self.ui_location = ConfigItem("UI", "location", "100x100")
self.ui_maximized = ConfigItem("UI", "maximized", False, BoolValidator())
self.notify_SendTaskResultTime = OptionsConfigItem(
"Notify",
"SendTaskResultTime",
"不推送",
OptionsValidator(["不推送", "任何时刻", "仅失败时"]),
)
self.notify_IfSendStatistic = ConfigItem(
"Notify", "IfSendStatistic", False, BoolValidator()
)
self.notify_IfSendSixStar = ConfigItem(
"Notify", "IfSendSixStar", False, BoolValidator()
)
self.notify_IfPushPlyer = ConfigItem(
"Notify", "IfPushPlyer", False, BoolValidator()
)
self.notify_IfSendMail = ConfigItem(
"Notify", "IfSendMail", False, BoolValidator()
)
self.notify_SMTPServerAddress = ConfigItem("Notify", "SMTPServerAddress", "")
self.notify_AuthorizationCode = ConfigItem("Notify", "AuthorizationCode", "")
self.notify_FromAddress = ConfigItem("Notify", "FromAddress", "")
self.notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
self.notify_IfServerChan = ConfigItem(
"Notify", "IfServerChan", False, BoolValidator()
)
self.notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
self.notify_ServerChanChannel = ConfigItem("Notify", "ServerChanChannel", "")
self.notify_ServerChanTag = ConfigItem("Notify", "ServerChanTag", "")
self.notify_IfCompanyWebHookBot = ConfigItem(
"Notify", "IfCompanyWebHookBot", False, BoolValidator()
)
self.notify_CompanyWebHookBotUrl = ConfigItem(
"Notify", "CompanyWebHookBotUrl", ""
)
self.update_IfAutoUpdate = ConfigItem(
"Update", "IfAutoUpdate", False, BoolValidator()
)
self.update_UpdateType = OptionsConfigItem(
"Update", "UpdateType", "stable", OptionsValidator(["stable", "beta"])
)
self.update_ThreadNumb = RangeConfigItem(
"Update", "ThreadNumb", 8, RangeValidator(1, 32)
)
self.update_ProxyUrlList = ConfigItem(
"Update", "ProxyUrlList", [], UrlListValidator()
)
self.update_MirrorChyanCDK = ConfigItem("Update", "MirrorChyanCDK", "")
class QueueConfig(LQConfig):
"""队列配置"""
def __init__(self) -> None:
@@ -260,8 +270,10 @@ class QueueConfig(QConfig):
self.queueSet_AfterAccomplish = OptionsConfigItem(
"QueueSet",
"AfterAccomplish",
"None",
OptionsValidator(["None", "KillSelf", "Sleep", "Hibernate", "Shutdown"]),
"NoAction",
OptionsValidator(
["NoAction", "KillSelf", "Sleep", "Hibernate", "Shutdown"]
),
)
self.time_TimeEnabled_0 = ConfigItem(
@@ -332,72 +344,8 @@ class QueueConfig(QConfig):
"Data", "LastProxyHistory", "暂无历史运行记录"
)
def toDict(self, serialize=True):
"""convert config items to `dict`"""
items = {}
for name in dir(self._cfg):
item = getattr(self._cfg, name)
if not isinstance(item, ConfigItem):
continue
value = item.serialize() if serialize else item.value
if not items.get(item.group):
if not item.name:
items[item.group] = value
else:
items[item.group] = {}
if item.name:
items[item.group][item.name] = value
return items
@exceptionHandler()
def load(self, file=None, config=None):
"""load config
Parameters
----------
file: str or Path
the path of json config file
config: Config
config object to be initialized
"""
if isinstance(config, QConfig):
self._cfg = config
self._cfg.themeChanged.connect(self.themeChanged)
if isinstance(file, (str, Path)):
self._cfg.file = Path(file)
try:
with open(self._cfg.file, encoding="utf-8") as f:
cfg = json.load(f)
except:
cfg = {}
# map config items'key to item
items = {}
for name in dir(self._cfg):
item = getattr(self._cfg, name)
if isinstance(item, ConfigItem):
items[item.key] = item
# update the value of config item
for k, v in cfg.items():
if not isinstance(v, dict) and items.get(k) is not None:
items[k].deserializeFrom(v)
elif isinstance(v, dict):
for key, value in v.items():
key = k + "." + key
if items.get(key) is not None:
items[key].deserializeFrom(value)
self.theme = self.get(self._cfg.themeMode)
class MaaConfig(QConfig):
class MaaConfig(LQConfig):
"""MAA配置"""
def __init__(self) -> None:
@@ -415,6 +363,9 @@ class MaaConfig(QConfig):
self.RunSet_ProxyTimesLimit = RangeConfigItem(
"RunSet", "ProxyTimesLimit", 0, RangeValidator(0, 1024)
)
self.RunSet_ADBSearchRange = RangeConfigItem(
"RunSet", "ADBSearchRange", 0, RangeValidator(0, 3)
)
self.RunSet_RunTimesLimit = RangeConfigItem(
"RunSet", "RunTimesLimit", 3, RangeValidator(1, 1024)
)
@@ -431,72 +382,8 @@ class MaaConfig(QConfig):
"RunSet", "AutoUpdateMaa", False, BoolValidator()
)
def toDict(self, serialize=True):
"""convert config items to `dict`"""
items = {}
for name in dir(self._cfg):
item = getattr(self._cfg, name)
if not isinstance(item, ConfigItem):
continue
value = item.serialize() if serialize else item.value
if not items.get(item.group):
if not item.name:
items[item.group] = value
else:
items[item.group] = {}
if item.name:
items[item.group][item.name] = value
return items
@exceptionHandler()
def load(self, file=None, config=None):
"""load config
Parameters
----------
file: str or Path
the path of json config file
config: Config
config object to be initialized
"""
if isinstance(config, QConfig):
self._cfg = config
self._cfg.themeChanged.connect(self.themeChanged)
if isinstance(file, (str, Path)):
self._cfg.file = Path(file)
try:
with open(self._cfg.file, encoding="utf-8") as f:
cfg = json.load(f)
except:
cfg = {}
# map config items'key to item
items = {}
for name in dir(self._cfg):
item = getattr(self._cfg, name)
if isinstance(item, ConfigItem):
items[item.key] = item
# update the value of config item
for k, v in cfg.items():
if not isinstance(v, dict) and items.get(k) is not None:
items[k].deserializeFrom(v)
elif isinstance(v, dict):
for key, value in v.items():
key = k + "." + key
if items.get(key) is not None:
items[key].deserializeFrom(value)
self.theme = self.get(self._cfg.themeMode)
class MaaUserConfig(QConfig):
class MaaUserConfig(LQConfig):
"""MAA用户配置"""
def __init__(self) -> None:
@@ -507,9 +394,7 @@ class MaaUserConfig(QConfig):
self.Info_Mode = OptionsConfigItem(
"Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"])
)
self.Info_GameIdMode = OptionsConfigItem(
"Info", "GameIdMode", "固定", OptionsValidator(["固定"])
)
self.Info_GameIdMode = ConfigItem("Info", "GameIdMode", "固定")
self.Info_Server = OptionsConfigItem(
"Info", "Server", "Official", OptionsValidator(["Official", "Bilibili"])
)
@@ -535,18 +420,21 @@ class MaaUserConfig(QConfig):
self.Info_SeriesNumb = OptionsConfigItem(
"Info",
"SeriesNumb",
"1",
OptionsValidator(["1000", "6", "5", "4", "3", "2", "1", "-1"]),
"0",
OptionsValidator(["0", "6", "5", "4", "3", "2", "1", "-1"]),
)
self.Info_GameId = ConfigItem("Info", "GameId", "-")
self.Info_GameId_1 = ConfigItem("Info", "GameId_1", "-")
self.Info_GameId_2 = ConfigItem("Info", "GameId_2", "-")
self.Info_GameId_Remain = ConfigItem("Info", "GameId_Remain", "-")
self.Info_IfSkland = ConfigItem("Info", "IfSkland", False, BoolValidator())
self.Info_SklandToken = ConfigItem("Info", "SklandToken", "")
self.Data_LastProxyDate = ConfigItem("Data", "LastProxyDate", "2000-01-01")
self.Data_LastAnnihilationDate = ConfigItem(
"Data", "LastAnnihilationDate", "2000-01-01"
)
self.Data_LastSklandDate = ConfigItem("Data", "LastSklandDate", "2000-01-01")
self.Data_ProxyTimes = ConfigItem(
"Data", "ProxyTimes", 0, RangeValidator(0, 1024)
)
@@ -555,78 +443,145 @@ class MaaUserConfig(QConfig):
"Data", "CustomInfrastPlanIndex", "0"
)
def toDict(self, serialize=True):
"""convert config items to `dict`"""
items = {}
for name in dir(self._cfg):
item = getattr(self._cfg, name)
if not isinstance(item, ConfigItem):
continue
self.Task_IfWakeUp = ConfigItem("Task", "IfWakeUp", True, BoolValidator())
self.Task_IfRecruiting = ConfigItem(
"Task", "IfRecruiting", True, BoolValidator()
)
self.Task_IfBase = ConfigItem("Task", "IfBase", True, BoolValidator())
self.Task_IfCombat = ConfigItem("Task", "IfCombat", True, BoolValidator())
self.Task_IfMall = ConfigItem("Task", "IfMall", True, BoolValidator())
self.Task_IfMission = ConfigItem("Task", "IfMission", True, BoolValidator())
self.Task_IfAutoRoguelike = ConfigItem(
"Task", "IfAutoRoguelike", False, BoolValidator()
)
self.Task_IfReclamation = ConfigItem(
"Task", "IfReclamation", False, BoolValidator()
)
value = item.serialize() if serialize else item.value
if not items.get(item.group):
if not item.name:
items[item.group] = value
else:
items[item.group] = {}
self.Notify_Enabled = ConfigItem("Notify", "Enabled", False, BoolValidator())
self.Notify_IfSendStatistic = ConfigItem(
"Notify", "IfSendStatistic", False, BoolValidator()
)
self.Notify_IfSendSixStar = ConfigItem(
"Notify", "IfSendSixStar", False, BoolValidator()
)
self.Notify_IfSendMail = ConfigItem(
"Notify", "IfSendMail", False, BoolValidator()
)
self.Notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
self.Notify_IfServerChan = ConfigItem(
"Notify", "IfServerChan", False, BoolValidator()
)
self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
self.Notify_ServerChanChannel = ConfigItem("Notify", "ServerChanChannel", "")
self.Notify_ServerChanTag = ConfigItem("Notify", "ServerChanTag", "")
self.Notify_IfCompanyWebHookBot = ConfigItem(
"Notify", "IfCompanyWebHookBot", False, BoolValidator()
)
self.Notify_CompanyWebHookBotUrl = ConfigItem(
"Notify", "CompanyWebHookBotUrl", ""
)
if item.name:
items[item.group][item.name] = value
def get_plan_info(self) -> Dict[str, Union[str, int]]:
"""获取当前的计划下信息"""
return items
if self.get(self.Info_GameIdMode) == "固定":
return {
"MedicineNumb": self.get(self.Info_MedicineNumb),
"SeriesNumb": self.get(self.Info_SeriesNumb),
"GameId": self.get(self.Info_GameId),
"GameId_1": self.get(self.Info_GameId_1),
"GameId_2": self.get(self.Info_GameId_2),
"GameId_Remain": self.get(self.Info_GameId_Remain),
}
elif "计划" in self.get(self.Info_GameIdMode):
plan = Config.plan_dict[self.get(self.Info_GameIdMode)]["Config"]
return {
"MedicineNumb": plan.get(plan.get_current_info("MedicineNumb")),
"SeriesNumb": plan.get(plan.get_current_info("SeriesNumb")),
"GameId": plan.get(plan.get_current_info("GameId")),
"GameId_1": plan.get(plan.get_current_info("GameId_1")),
"GameId_2": plan.get(plan.get_current_info("GameId_2")),
"GameId_Remain": plan.get(plan.get_current_info("GameId_Remain")),
}
@exceptionHandler()
def load(self, file=None, config=None):
"""load config
Parameters
----------
file: str or Path
the path of json config file
class MaaPlanConfig(LQConfig):
"""MAA计划表配置"""
config: Config
config object to be initialized
"""
if isinstance(config, QConfig):
self._cfg = config
self._cfg.themeChanged.connect(self.themeChanged)
def __init__(self) -> None:
super().__init__()
if isinstance(file, (str, Path)):
self._cfg.file = Path(file)
self.Info_Name = ConfigItem("Info", "Name", "")
self.Info_Mode = OptionsConfigItem(
"Info", "Mode", "ALL", OptionsValidator(["ALL", "Weekly"])
)
try:
with open(self._cfg.file, encoding="utf-8") as f:
cfg = json.load(f)
except:
cfg = {}
self.config_item_dict: dict[str, Dict[str, ConfigItem]] = {}
# map config items'key to item
items = {}
for name in dir(self._cfg):
item = getattr(self._cfg, name)
if isinstance(item, ConfigItem):
items[item.key] = item
for group in [
"ALL",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
]:
self.config_item_dict[group] = {}
# update the value of config item
for k, v in cfg.items():
if not isinstance(v, dict) and items.get(k) is not None:
items[k].deserializeFrom(v)
elif isinstance(v, dict):
for key, value in v.items():
key = k + "." + key
if items.get(key) is not None:
items[key].deserializeFrom(value)
self.config_item_dict[group]["MedicineNumb"] = ConfigItem(
group, "MedicineNumb", 0, RangeValidator(0, 1024)
)
self.config_item_dict[group]["SeriesNumb"] = OptionsConfigItem(
group,
"SeriesNumb",
"0",
OptionsValidator(["0", "6", "5", "4", "3", "2", "1", "-1"]),
)
self.config_item_dict[group]["GameId"] = ConfigItem(group, "GameId", "-")
self.config_item_dict[group]["GameId_1"] = ConfigItem(
group, "GameId_1", "-"
)
self.config_item_dict[group]["GameId_2"] = ConfigItem(
group, "GameId_2", "-"
)
self.config_item_dict[group]["GameId_Remain"] = ConfigItem(
group, "GameId_Remain", "-"
)
self.theme = self.get(self._cfg.themeMode)
for name in [
"MedicineNumb",
"SeriesNumb",
"GameId",
"GameId_1",
"GameId_2",
"GameId_Remain",
]:
setattr(self, f"{group}_{name}", self.config_item_dict[group][name])
def get_current_info(self, name: str) -> ConfigItem:
"""获取当前的计划表配置项"""
if self.get(self.Info_Mode) == "ALL":
return self.config_item_dict["ALL"][name]
elif self.get(self.Info_Mode) == "Weekly":
today = datetime.now().strftime("%A")
if today in self.config_item_dict:
return self.config_item_dict[today][name]
else:
return self.config_item_dict["ALL"][name]
class AppConfig(GlobalConfig):
VERSION = "4.3.6.0"
VERSION = "4.3.10.0"
gameid_refreshed = Signal()
PASSWORD_refreshed = Signal()
user_info_changed = Signal()
power_sign_changed = Signal()
def __init__(self) -> None:
super().__init__()
@@ -645,13 +600,42 @@ class AppConfig(GlobalConfig):
self.PASSWORD = ""
self.running_list = []
self.silence_list = []
self.info_bar_list = []
self.gameid_dict = {
"ALL": {"value": [], "text": []},
"Today": {"value": [], "text": []},
"Monday": {"value": [], "text": []},
"Tuesday": {"value": [], "text": []},
"Wednesday": {"value": [], "text": []},
"Thursday": {"value": [], "text": []},
"Friday": {"value": [], "text": []},
"Saturday": {"value": [], "text": []},
"Sunday": {"value": [], "text": []},
}
self.power_sign = "NoAction"
self.if_ignore_silence = False
self.if_database_opened = False
self.search_member()
self.search_queue()
parser = argparse.ArgumentParser(
prog="AUTO_MAA",
description="A MAA Multi Account Management and Automation Tool",
)
parser.add_argument(
"--mode",
choices=["gui", "cli"],
default="gui",
help="使用UI界面或命令行模式运行程序",
)
parser.add_argument(
"--config",
nargs="+",
choices=list(self.member_dict.keys()) + list(self.queue_dict.keys()),
help="指定需要运行哪些配置项",
)
self.args = parser.parse_args()
self.initialize()
def initialize(self) -> None:
@@ -674,7 +658,8 @@ class AppConfig(GlobalConfig):
def init_logger(self) -> None:
"""初始化日志记录器"""
logger.remove(0)
if self.args.mode != "cli":
logger.remove(0)
logger.add(
sink=self.log_path,
@@ -687,10 +672,14 @@ class AppConfig(GlobalConfig):
retention="1 month",
compression="zip",
)
logger.info("")
logger.info("===================================")
logger.info("AUTO_MAA 主程序")
logger.info(f"版本号: v{self.VERSION}")
logger.info(f"根目录: {self.app_path}")
logger.info(
f"运行模式: {'图形化界面' if self.args.mode == 'gui' else '命令行界面'}"
)
logger.info("===================================")
logger.info("日志记录器初始化完成")
@@ -698,21 +687,23 @@ class AppConfig(GlobalConfig):
def get_gameid(self) -> None:
# 从MAA服务器获取活动关卡信息
Network.set_info(
network = Network.add_task(
mode="get",
url="https://ota.maa.plus/MaaAssistantArknights/api/gui/StageActivity.json",
url="https://api.maa.plus/MaaAssistantArknights/api/gui/StageActivity.json",
)
Network.start()
Network.loop.exec()
if Network.stutus_code == 200:
network.loop.exec()
network_result = Network.get_result(network)
if network_result["status_code"] == 200:
gameid_infos: List[Dict[str, Union[str, Dict[str, Union[str, int]]]]] = (
Network.response_json["Official"]["sideStoryStage"]
network_result["response_json"]["Official"]["sideStoryStage"]
)
else:
logger.warning(f"无法从MAA服务器获取活动关卡信息:{Network.error_message}")
logger.warning(
f"无法从MAA服务器获取活动关卡信息:{network_result['error_message']}"
)
gameid_infos = []
gameid_dict = {"value": [], "text": []}
ss_gameid_dict = {"value": [], "text": []}
for gameid_info in gameid_infos:
@@ -725,53 +716,11 @@ class AppConfig(GlobalConfig):
gameid_info["Activity"]["UtcExpireTime"], "%Y/%m/%d %H:%M:%S"
)
):
gameid_dict["value"].append(gameid_info["Value"])
gameid_dict["text"].append(gameid_info["Value"])
ss_gameid_dict["value"].append(gameid_info["Value"])
ss_gameid_dict["text"].append(gameid_info["Value"])
# 生成全部关卡信息
self.gameid_dict["ALL"]["value"] = gameid_dict["value"] + [
"-",
"1-7",
"R8-11",
"12-17-HARD",
"CE-6",
"AP-5",
"CA-5",
"LS-6",
"SK-5",
"PR-A-1",
"PR-A-2",
"PR-B-1",
"PR-B-2",
"PR-C-1",
"PR-C-2",
"PR-D-1",
"PR-D-2",
]
self.gameid_dict["ALL"]["text"] = gameid_dict["text"] + [
"当前/上次",
"1-7",
"R8-11",
"12-17-HARD",
"龙门币-6/5",
"红票-5",
"技能-5",
"经验-6/5",
"碳-5",
"奶/盾芯片",
"奶/盾芯片组",
"术/狙芯片",
"术/狙芯片组",
"先/辅芯片",
"先/辅芯片组",
"近/特芯片",
"近/特芯片组",
]
# 生成本日关卡信息
days = self.server_date().isoweekday()
gameid_list = [
# 生成每日关卡信息
gameid_daily_info = [
{"value": "-", "text": "当前/上次", "days": [1, 2, 3, 4, 5, 6, 7]},
{"value": "1-7", "text": "1-7", "days": [1, 2, 3, 4, 5, 6, 7]},
{"value": "R8-11", "text": "R8-11", "days": [1, 2, 3, 4, 5, 6, 7]},
@@ -795,12 +744,20 @@ class AppConfig(GlobalConfig):
{"value": "PR-D-2", "text": "近/特芯片组", "days": [2, 3, 6, 7]},
]
for gameid_info in gameid_list:
if days in gameid_info["days"]:
gameid_dict["value"].append(gameid_info["value"])
gameid_dict["text"].append(gameid_info["text"])
for day in range(0, 8):
self.gameid_dict["Today"] = gameid_dict
today_gameid_dict = {"value": [], "text": []}
for gameid_info in gameid_daily_info:
if day in gameid_info["days"] or day == 0:
today_gameid_dict["value"].append(gameid_info["value"])
today_gameid_dict["text"].append(gameid_info["text"])
self.gameid_dict[calendar.day_name[day - 1] if day > 0 else "ALL"] = {
"value": today_gameid_dict["value"] + ss_gameid_dict["value"],
"text": today_gameid_dict["text"] + ss_gameid_dict["text"],
}
self.gameid_refreshed.emit()
@@ -1231,6 +1188,28 @@ class AppConfig(GlobalConfig):
sorted(user_dict.items(), key=lambda x: int(x[0][3:]))
)
def search_plan(self) -> None:
"""搜索所有计划表"""
self.plan_dict: Dict[str, Dict[str, Union[str, Path, MaaPlanConfig]]] = {}
if (self.app_path / "config/MaaPlanConfig").exists():
for maa_plan_dir in (self.app_path / "config/MaaPlanConfig").iterdir():
if maa_plan_dir.is_dir():
maa_plan_config = MaaPlanConfig()
maa_plan_config.load(maa_plan_dir / "config.json", maa_plan_config)
maa_plan_config.save()
self.plan_dict[maa_plan_dir.name] = {
"Type": "Maa",
"Path": maa_plan_dir,
"Config": maa_plan_config,
}
self.plan_dict = dict(
sorted(self.plan_dict.items(), key=lambda x: int(x[0][3:]))
)
def search_queue(self):
"""搜索所有调度队列实例"""
@@ -1278,6 +1257,16 @@ class AppConfig(GlobalConfig):
if queue["Config"].get(queue["Config"].queue_Member_10) == old:
queue["Config"].set(queue["Config"].queue_Member_10, new)
def change_plan(self, old: str, new: str) -> None:
"""修改脚本管理所有下属用户的计划表配置参数"""
for member in self.member_dict.values():
for user in member["UserData"].values():
if user["Config"].get(user["Config"].Info_GameIdMode) == old:
user["Config"].set(user["Config"].Info_GameIdMode, new)
def change_user_info(
self, name: str, user_data: Dict[str, Dict[str, Union[str, Path, dict]]]
) -> None:
@@ -1297,6 +1286,10 @@ class AppConfig(GlobalConfig):
user_config.Data_LastAnnihilationDate,
info["Config"]["Data"]["LastAnnihilationDate"],
)
user_config.set(
user_config.Data_LastSklandDate,
info["Config"]["Data"]["LastSklandDate"],
)
user_config.set(
user_config.Data_ProxyTimes, info["Config"]["Data"]["ProxyTimes"]
)
@@ -1310,6 +1303,12 @@ class AppConfig(GlobalConfig):
self.user_info_changed.emit()
def set_power_sign(self, sign: str) -> None:
"""设置当前电源状态"""
self.power_sign = sign
self.power_sign_changed.emit()
def save_history(self, key: str, content: dict) -> None:
"""保存历史记录"""

View File

@@ -30,28 +30,36 @@ from PySide6.QtCore import Qt
from qfluentwidgets import InfoBar, InfoBarPosition
from .config import Config
from .sound_player import SoundPlayer
class _MainInfoBar:
"""信息通知栏"""
def push_info_bar(self, mode: str, title: str, content: str, time: int):
# 模式到 InfoBar 方法的映射
mode_mapping = {
"success": InfoBar.success,
"warning": InfoBar.warning,
"error": InfoBar.error,
"info": InfoBar.info,
}
def push_info_bar(
self, mode: str, title: str, content: str, time: int, if_force: bool = False
):
"""推送到信息通知栏"""
if Config.main_window is None:
logger.error("信息通知栏未设置父窗口")
return None
# 定义模式到 InfoBar 方法的映射
mode_mapping = {
"success": InfoBar.success,
"warning": InfoBar.warning,
"error": InfoBar.error,
"info": InfoBar.info,
}
# 根据 mode 获取对应的 InfoBar 方法
info_bar_method = mode_mapping.get(mode)
if info_bar_method:
info_bar_method = self.mode_mapping.get(mode)
if not info_bar_method:
logger.error(f"未知的通知栏模式: {mode}")
return None
if Config.main_window.isVisible():
info_bar_method(
title=title,
content=content,
@@ -61,8 +69,21 @@ class _MainInfoBar:
duration=time,
parent=Config.main_window,
)
else:
logger.error(f"未知的通知栏模式: {mode}")
elif if_force:
# 如果主窗口不可见且强制推送,则录入消息队列等待窗口显示后推送
info_bar_item = {
"mode": mode,
"title": title,
"content": content,
"time": time,
}
if info_bar_item not in Config.info_bar_list:
Config.info_bar_list.append(info_bar_item)
if mode == "warning":
SoundPlayer.play("发生异常")
if mode == "error":
SoundPlayer.play("发生错误")
MainInfoBar = _MainInfoBar()

View File

@@ -26,55 +26,46 @@ v4.3
"""
from loguru import logger
from PySide6.QtCore import QThread, QEventLoop, QTimer
from PySide6.QtCore import QObject, QThread, QEventLoop
import re
import time
import requests
from pathlib import Path
class _Network(QThread):
class NetworkThread(QThread):
"""网络请求线程类"""
max_retries = 3
timeout = 10
backoff_factor = 0.1
def __init__(self) -> None:
def __init__(self, mode: str, url: str, path: Path = None) -> None:
super().__init__()
self.if_running = False
self.mode = None
self.url = None
self.loop = QEventLoop()
self.wait_loop = QEventLoop()
@logger.catch
def run(self) -> None:
"""运行网络请求线程"""
self.if_running = True
if self.mode == "get":
self.get_json(self.url)
elif self.mode == "get_file":
self.get_file(self.url, self.path)
self.if_running = False
def set_info(self, mode: str, url: str, path: Path = None) -> None:
"""设置网络请求信息"""
while self.if_running:
QTimer.singleShot(self.backoff_factor * 1000, self.wait_loop.quit)
self.wait_loop.exec()
self.setObjectName(
f"NetworkThread-{mode}-{re.sub(r'(&cdk=)[^&]+(&)', r'\1******\2', url)}"
)
self.mode = mode
self.url = url
self.path = path
self.stutus_code = None
self.status_code = None
self.response_json = None
self.error_message = None
self.loop = QEventLoop()
@logger.catch
def run(self) -> None:
"""运行网络请求线程"""
if self.mode == "get":
self.get_json(self.url)
elif self.mode == "get_file":
self.get_file(self.url, self.path)
def get_json(self, url: str) -> None:
"""通过get方法获取json数据"""
@@ -83,12 +74,12 @@ class _Network(QThread):
for _ in range(self.max_retries):
try:
response = requests.get(url, timeout=self.timeout)
self.stutus_code = response.status_code
self.status_code = response.status_code
self.response_json = response.json()
self.error_message = None
break
except Exception as e:
self.stutus_code = response.status_code if response else None
self.status_code = response.status_code if response else None
self.response_json = None
self.error_message = str(e)
time.sleep(self.backoff_factor)
@@ -105,16 +96,56 @@ class _Network(QThread):
if response.status_code == 200:
with open(path, "wb") as file:
file.write(response.content)
self.stutus_code = response.status_code
self.status_code = response.status_code
else:
self.stutus_code = response.status_code
self.status_code = response.status_code
self.error_message = "下载失败"
except Exception as e:
self.stutus_code = response.status_code if response else None
self.status_code = response.status_code if response else None
self.error_message = str(e)
self.loop.quit()
class _Network(QObject):
"""网络请求线程类"""
def __init__(self) -> None:
super().__init__()
self.task_queue = []
def add_task(self, mode: str, url: str, path: Path = None) -> NetworkThread:
"""添加网络请求任务"""
network_thread = NetworkThread(mode, url, path)
self.task_queue.append(network_thread)
network_thread.start()
return network_thread
def get_result(self, network_thread: NetworkThread) -> dict:
"""获取网络请求结果"""
result = {
"status_code": network_thread.status_code,
"response_json": network_thread.response_json,
"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()

69
app/core/sound_player.py Normal file
View File

@@ -0,0 +1,69 @@
# 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.3
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtCore import QObject, QUrl
from PySide6.QtMultimedia import QSoundEffect
from pathlib import Path
from .config import Config
class _SoundPlayer(QObject):
def __init__(self):
super().__init__()
self.sounds_path = Config.app_path / "resources/sounds"
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")
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"
)
def play_voice(self, sound_path: Path):
effect = QSoundEffect(self)
effect.setVolume(1)
effect.setSource(QUrl.fromLocalFile(sound_path))
effect.play()
SoundPlayer = _SoundPlayer()

View File

@@ -35,6 +35,7 @@ from typing import Dict, Union
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.services import System
@@ -44,6 +45,7 @@ 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)
@@ -59,6 +61,8 @@ class Task(QThread):
):
super(Task, self).__init__()
self.setObjectName(f"Task-{mode}-{name}")
self.mode = mode
self.name = name
self.info = info
@@ -82,6 +86,7 @@ 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()
@@ -141,6 +146,7 @@ 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)
@@ -191,6 +197,7 @@ class _TaskManager(QObject):
logger.info(f"任务开始:{name}")
MainInfoBar.push_info_bar("info", "任务开始", name, 3000)
SoundPlayer.play("任务开始")
Config.running_list.append(name)
self.task_dict[name] = Task(mode, name, info)
@@ -199,6 +206,7 @@ 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].play_sound.connect(SoundPlayer.play)
self.task_dict[name].update_user_info.connect(Config.change_user_info)
self.task_dict[name].accomplish.connect(
lambda logs: self.remove_task(mode, name, logs)
@@ -239,6 +247,7 @@ class _TaskManager(QObject):
logger.info(f"任务结束:{name}")
MainInfoBar.push_info_bar("info", "任务结束", name, 3000)
SoundPlayer.play("任务结束")
self.task_dict[name].deleteLater()
self.task_dict.pop(name)
@@ -265,46 +274,35 @@ class _TaskManager(QObject):
Config.queue_dict[name]["Config"].get(
Config.queue_dict[name]["Config"].queueSet_AfterAccomplish
)
!= "None"
!= "NoAction"
and Config.power_sign == "NoAction"
):
from app.ui import ProgressRingMessageBox
mode_book = {
"Shutdown": "关机",
"Hibernate": "休眠",
"Sleep": "睡眠",
"KillSelf": "关闭AUTO_MAA",
}
choice = ProgressRingMessageBox(
Config.main_window,
f"{mode_book[Config.queue_dict[name]["Config"].get(Config.queue_dict[name]["Config"].queueSet_AfterAccomplish)]}倒计时",
)
if choice.exec():
System.set_power(
Config.queue_dict[name]["Config"].get(
Config.queue_dict[name]["Config"].queueSet_AfterAccomplish
)
Config.set_power_sign(
Config.queue_dict[name]["Config"].get(
Config.queue_dict[name]["Config"].queueSet_AfterAccomplish
)
)
if Config.args.mode == "cli" and Config.power_sign == "NoAction":
Config.set_power_sign("KillSelf")
def check_maa_version(self, v: str):
"""检查MAA版本"""
Network.set_info(
network = Network.add_task(
mode="get",
url="https://mirrorchyan.com/api/resources/MAA/latest?user_agent=AutoMaaGui&os=win&arch=x64&channel=stable",
)
Network.start()
Network.loop.exec()
if Network.stutus_code == 200:
maa_info = Network.response_json
network.loop.exec()
network_result = Network.get_result(network)
if network_result["status_code"] == 200:
maa_info = network_result["response_json"]
else:
logger.warning(f"获取MAA版本信息时出错{Network.error_message}")
logger.warning(f"获取MAA版本信息时出错{network_result['error_message']}")
MainInfoBar.push_info_bar(
"warning",
"获取MAA版本信息时出错",
f"网络错误:{Network.stutus_code}",
f"网络错误:{network_result['status_code']}",
5000,
)
return None

View File

@@ -26,26 +26,25 @@ v4.3
"""
from loguru import logger
from PySide6.QtWidgets import QWidget
from PySide6.QtCore import QTimer
from PySide6.QtCore import QObject, QTimer
from datetime import datetime
import pyautogui
from pathlib import Path
import keyboard
from .config import Config
from .task_manager import TaskManager
from app.services import System
class _MainTimer(QWidget):
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)
self.Timer.timeout.connect(self.check_power)
self.Timer.start(1000)
self.LongTimer = QTimer()
self.LongTimer.timeout.connect(self.long_timed_task)
@@ -96,22 +95,44 @@ class _MainTimer(QWidget):
):
windows = System.get_window_info()
# 此处排除雷电名为新通知的窗口
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):
if Config.power_sign != "NoAction" and not Config.running_list:
from app.ui import ProgressRingMessageBox
mode_book = {
"KillSelf": "退出软件",
"Sleep": "睡眠",
"Hibernate": "休眠",
"Shutdown": "关机",
}
choice = ProgressRingMessageBox(
Config.main_window, f"{mode_book[Config.power_sign]}倒计时"
)
if choice.exec():
System.set_power(Config.power_sign)
Config.set_power_sign("NoAction")
else:
Config.set_power_sign("NoAction")
MainTimer = _MainTimer()

View File

@@ -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
from app.services import Notify, System
from app.services import Notify, Crypto, System, skland_sign_in
from app.utils.ImageUtils import ImageUtils
class MaaManager(QObject):
@@ -50,6 +51,7 @@ 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)
@@ -88,6 +90,8 @@ class MaaManager(QObject):
self.question_response.connect(self.__capture_response)
self.question_response.connect(self.question_loop.quit)
self.wait_loop = QEventLoop()
self.interrupt.connect(self.quit_monitor)
self.maa_version = None
@@ -102,6 +106,9 @@ class MaaManager(QObject):
"Path": info["Path"],
"Config": info["Config"].toDict(),
}
planed_info = info["Config"].get_plan_info()
for key, value in planed_info.items():
self.data[name]["Config"]["Info"][key] = value
self.data = dict(sorted(self.data.items(), key=lambda x: int(x[0][3:])))
@@ -114,10 +121,15 @@ class MaaManager(QObject):
self.maa_log_path = self.maa_root_path / "debug/gui.log"
self.maa_exe_path = self.maa_root_path / "MAA.exe"
self.maa_tasks_path = self.maa_root_path / "resource/tasks/tasks.json"
self.port_range = [0] + [
(i // 2 + 1) * (-1 if i % 2 else 1)
for i in range(0, 2 * self.set["RunSet"]["ADBSearchRange"])
]
def run(self):
"""主进程运行MAA代理进程"""
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")
@@ -209,6 +221,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"]:
@@ -266,9 +331,23 @@ class MaaManager(QObject):
)
# 解析任务构成
if user_data["Info"]["Mode"] == "简洁":
if mode == "Routine":
self.task_dict = {
"WakeUp": str(user_data["Task"]["IfWakeUp"]),
"Recruiting": str(user_data["Task"]["IfRecruiting"]),
"Base": str(user_data["Task"]["IfBase"]),
"Combat": str(user_data["Task"]["IfCombat"]),
"Mission": str(user_data["Task"]["IfMission"]),
"Mall": str(user_data["Task"]["IfMall"]),
"AutoRoguelike": str(user_data["Task"]["IfAutoRoguelike"]),
"Reclamation": str(user_data["Task"]["IfReclamation"]),
}
elif mode == "Annihilation":
if user_data["Info"]["Mode"] == "简洁":
if mode == "Annihilation":
self.task_dict = {
"WakeUp": "True",
"Recruiting": "False",
@@ -280,52 +359,40 @@ class MaaManager(QObject):
"Reclamation": "False",
}
elif mode == "Routine":
elif user_data["Info"]["Mode"] == "详细":
with (self.data[user[2]]["Path"] / f"{mode}/gui.json").open(
mode="r", encoding="utf-8"
) as f:
data = json.load(f)
self.task_dict = {
"WakeUp": "True",
"Recruiting": "True",
"Base": "True",
"Combat": "True",
"Mission": "True",
"Mall": "True",
"AutoRoguelike": "False",
"Reclamation": "False",
"WakeUp": data["Configurations"]["Default"][
"TaskQueue.WakeUp.IsChecked"
],
"Recruiting": data["Configurations"]["Default"][
"TaskQueue.Recruiting.IsChecked"
],
"Base": data["Configurations"]["Default"][
"TaskQueue.Base.IsChecked"
],
"Combat": data["Configurations"]["Default"][
"TaskQueue.Combat.IsChecked"
],
"Mission": data["Configurations"]["Default"][
"TaskQueue.Mission.IsChecked"
],
"Mall": data["Configurations"]["Default"][
"TaskQueue.Mall.IsChecked"
],
"AutoRoguelike": data["Configurations"]["Default"][
"TaskQueue.AutoRoguelike.IsChecked"
],
"Reclamation": data["Configurations"]["Default"][
"TaskQueue.Reclamation.IsChecked"
],
}
elif user_data["Info"]["Mode"] == "详细":
with (self.data[user[2]]["Path"] / f"{mode}/gui.json").open(
mode="r", encoding="utf-8"
) as f:
data = json.load(f)
self.task_dict = {
"WakeUp": data["Configurations"]["Default"][
"TaskQueue.WakeUp.IsChecked"
],
"Recruiting": data["Configurations"]["Default"][
"TaskQueue.Recruiting.IsChecked"
],
"Base": data["Configurations"]["Default"][
"TaskQueue.Base.IsChecked"
],
"Combat": data["Configurations"]["Default"][
"TaskQueue.Combat.IsChecked"
],
"Mission": data["Configurations"]["Default"][
"TaskQueue.Mission.IsChecked"
],
"Mall": data["Configurations"]["Default"][
"TaskQueue.Mall.IsChecked"
],
"AutoRoguelike": data["Configurations"]["Default"][
"TaskQueue.AutoRoguelike.IsChecked"
],
"Reclamation": data["Configurations"]["Default"][
"TaskQueue.Reclamation.IsChecked"
],
}
# 尝试次数循环
for i in range(self.set["RunSet"]["RunTimesLimit"]):
@@ -386,6 +453,12 @@ class MaaManager(QObject):
self.if_open_emulator = True
break
self.wait_time = int(
set["Configurations"]["Default"][
"Start.EmulatorWaitSeconds"
]
)
self.ADB_path = Path(
set["Configurations"]["Default"]["Connect.AdbPath"]
)
@@ -410,6 +483,7 @@ class MaaManager(QObject):
# 任务开始前释放ADB
try:
logger.info(f"{self.name} | 释放ADB{self.ADB_address}")
subprocess.run(
[self.ADB_path, "disconnect", self.ADB_address],
creationflags=subprocess.CREATE_NO_WINDOW,
@@ -428,6 +502,9 @@ class MaaManager(QObject):
if self.if_open_emulator_process:
try:
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,
@@ -446,6 +523,8 @@ class MaaManager(QObject):
# 添加静默进程标记
Config.silence_list.append(self.emulator_path)
self.search_ADB_address()
# 创建MAA任务
maa = subprocess.Popen(
[self.maa_exe_path],
@@ -489,10 +568,7 @@ class MaaManager(QObject):
"检测到MAA进程完成代理任务\n正在等待相关程序结束\n请等待10s"
)
for _ in range(10):
if self.isInterruptionRequested:
break
time.sleep(1)
self.sleep(10)
else:
logger.error(
f"{self.name} | 用户: {user[0]} - 代理任务异常: {self.maa_result}"
@@ -517,6 +593,12 @@ class MaaManager(QObject):
) as f:
data = json.load(f)
# 记录自定义基建索引
if self.task_dict["Base"] == "False":
user_data["Data"]["CustomInfrastPlanIndex"] = data[
"Configurations"
]["Default"]["Infrast.CustomInfrastPlanIndex"]
# 记录更新包路径
if (
data["Global"]["VersionUpdate.package"]
@@ -536,16 +618,15 @@ class MaaManager(QObject):
f"{user[0].replace("_", " ")}{mode_book[mode][5:7]}出现异常",
1,
)
for _ in range(10):
if self.isInterruptionRequested:
break
time.sleep(1)
# 移除静默进程标记
Config.silence_list.remove(self.emulator_path)
if i == self.set["RunSet"]["RunTimesLimit"] - 1:
self.play_sound.emit("子任务失败")
else:
self.play_sound.emit(self.maa_result)
self.sleep(10)
# 任务结束后释放ADB
try:
logger.info(f"{self.name} | 释放ADB{self.ADB_address}")
subprocess.run(
[self.ADB_path, "disconnect", self.ADB_address],
creationflags=subprocess.CREATE_NO_WINDOW,
@@ -567,29 +648,6 @@ class MaaManager(QObject):
self.emulator_process.wait()
self.if_open_emulator = True
# 执行MAA解压更新动作
if self.maa_update_package:
logger.info(
f"{self.name} | 检测到MAA更新正在执行更新动作"
)
self.update_log_text.emit(
f"检测到MAA存在更新\nMAA正在执行更新动作\n请等待10s"
)
self.set_maa("更新MAA", None)
subprocess.Popen(
[self.maa_exe_path],
creationflags=subprocess.CREATE_NO_WINDOW,
)
for _ in range(10):
if self.isInterruptionRequested:
break
time.sleep(1)
System.kill_process(self.maa_exe_path)
logger.info(f"{self.name} | 更新动作结束")
# 记录剿灭情况
if (
mode == "Annihilation"
@@ -607,33 +665,57 @@ class MaaManager(QObject):
Config.app_path
/ f"history/{curdate}/{user_data["Info"]["Name"]}/{start_time.strftime("%H-%M-%S")}.json",
)
if Config.get(Config.notify_IfSendSixStar) and if_six_star:
if if_six_star:
self.push_notification(
"公招六星",
f"喜报:用户 {user[0]} 公招出六星啦!",
{"user_name": user_data["Info"]["Name"]},
{
"user_name": user_data["Info"]["Name"],
},
user_data,
)
self.play_sound.emit("六星喜报")
# 执行MAA解压更新动作
if self.maa_update_package:
logger.info(
f"{self.name} | 检测到MAA更新正在执行更新动作"
)
if Config.get(Config.notify_IfSendStatistic):
self.update_log_text.emit(
f"检测到MAA存在更新\nMAA正在执行更新动作\n请等待10s"
)
self.play_sound.emit("MAA更新")
self.set_maa("更新MAA", None)
subprocess.Popen(
[self.maa_exe_path],
creationflags=subprocess.CREATE_NO_WINDOW,
)
self.sleep(10)
System.kill_process(self.maa_exe_path)
statistics = Config.merge_maa_logs("指定项", user_logs_list)
statistics["user_info"] = user[0]
statistics["start_time"] = user_start_time.strftime(
"%Y-%m-%d %H:%M:%S"
)
statistics["end_time"] = datetime.now().strftime(
"%Y-%m-%d %H:%M:%S"
)
statistics["maa_result"] = (
"代理任务全部完成"
if (run_book["Annihilation"] and run_book["Routine"])
else "代理任务全部完成"
)
self.push_notification(
"统计信息", f"用户 {user[0]} 的自动代理统计报告", statistics
)
self.maa_update_package = ""
logger.info(f"{self.name} | 更新动作结束")
# 发送统计信息
statistics = Config.merge_maa_logs("指定项", 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")
statistics["end_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
statistics["maa_result"] = (
"代理任务全部完成"
if (run_book["Annihilation"] and run_book["Routine"])
else "代理任务未全部完成"
)
self.push_notification(
"统计信息",
f"{current_date} | 用户 {user[0]} 的自动代理统计报告",
statistics,
user_data,
)
if run_book["Annihilation"] and run_book["Routine"]:
# 成功完成代理的用户修改相关参数
@@ -719,10 +801,7 @@ class MaaManager(QObject):
# 无命令行中止MAA与其子程序
System.kill_process(self.maa_exe_path)
self.if_open_emulator = True
for _ in range(10):
if self.isInterruptionRequested:
break
time.sleep(1)
self.sleep(10)
# 登录成功,结束循环
if run_book[0]:
@@ -730,6 +809,7 @@ class MaaManager(QObject):
# 登录失败,询问是否结束循环
elif not self.isInterruptionRequested:
self.play_sound.emit("排查重试")
if not self.push_question(
"操作提示", "MAA未能正确登录到PRTS是否重试"
):
@@ -738,6 +818,7 @@ class MaaManager(QObject):
# 登录成功,录入人工排查情况
if run_book[0] and not self.isInterruptionRequested:
self.play_sound.emit("排查录入")
if self.push_question(
"操作提示", "请检查用户代理情况,该用户是否正确完成代理任务?"
):
@@ -804,9 +885,9 @@ class MaaManager(QObject):
# 保存运行日志
title = (
f"{self.set["MaaSet"]["Name"]}{self.mode[:4]}任务报告"
f"{current_date} | {self.set["MaaSet"]["Name"]}{self.mode[:4]}任务报告"
if self.set["MaaSet"]["Name"] != ""
else f"{self.mode[:4]}任务报告"
else f"{current_date} | {self.mode[:4]}任务报告"
)
result = {
"title": f"{self.mode[:4]}任务报告",
@@ -826,6 +907,17 @@ class MaaManager(QObject):
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_user"]) > 0:
result_text += f"{self.mode[2:4]}未成功的用户:\n{"\n".join(result["failed_user"])}\n"
if len(result["waiting_user"]) > 0:
result_text += f"\n未开始{self.mode[2:4]}的用户:\n{"\n".join(result["waiting_user"])}\n"
# 推送代理结果通知
Notify.push_plyer(
title.replace("报告", "已完成!"),
@@ -833,15 +925,7 @@ class MaaManager(QObject):
f"已完成用户数:{len(over_index)},未完成用户数:{len(error_index) + len(wait_index)}",
10,
)
if Config.get(Config.notify_SendTaskResultTime) == "任何时刻" or (
Config.get(Config.notify_SendTaskResultTime) == "仅失败时"
and len(error_index) + len(wait_index) != 0
):
result_text = self.push_notification("代理结果", title, result)
else:
result_text = self.push_notification(
"代理结果", title, result, if_get_text_only=True
)
self.push_notification("代理结果", title, result)
self.agree_bilibili(False)
self.log_monitor.deleteLater()
@@ -856,6 +940,7 @@ class MaaManager(QObject):
self.maa_result = "任务被手动中止"
self.isInterruptionRequested = True
self.wait_loop.quit()
def push_question(self, title: str, message: str) -> bool:
@@ -866,6 +951,100 @@ class MaaManager(QObject):
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 search_ADB_address(self) -> None:
"""搜索ADB实际地址"""
self.update_log_text.emit(
f"即将搜索ADB实际地址\n正在等待模拟器完成启动\n请等待{self.wait_time}s"
)
self.sleep(self.wait_time)
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]}-"
ADB_port = int(self.ADB_address.split("-")[1])
elif ":" in self.ADB_address:
ADB_ip = f"{self.ADB_address.split(':')[0]}:"
ADB_port = int(self.ADB_address.split(":")[1])
logger.info(
f"{self.name} | 正在搜索ADB实际地址ADB前缀{ADB_ip},初始端口:{ADB_port},搜索范围:{self.port_range}"
)
for port in self.port_range:
ADB_address = f"{ADB_ip}{ADB_port + port}"
# 尝试通过ADB连接到指定地址
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",
)
if "connected" in connect_result.stdout:
# 检查连接状态
devices_result = subprocess.run(
[self.ADB_path, "devices"],
creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
encoding="utf-8",
)
if ADB_address in devices_result.stdout:
logger.info(f"{self.name} | ADB实际地址{ADB_address}")
# 断开连接
subprocess.run(
[self.ADB_path, "disconnect", ADB_address],
creationflags=subprocess.CREATE_NO_WINDOW,
)
self.ADB_address = ADB_address
# 覆写当前ADB地址
System.kill_process(self.maa_exe_path)
with self.maa_set_path.open(mode="r", encoding="utf-8") as f:
data = json.load(f)
data["Configurations"]["Default"][
"Connect.Address"
] = self.ADB_address
data["Configurations"]["Default"]["Start.EmulatorWaitSeconds"] = "0"
with self.maa_set_path.open(mode="w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
self.play_sound.emit("ADB成功")
return None
else:
logger.info(f"{self.name} | 无法连接到ADB地址{ADB_address}")
else:
logger.info(f"{self.name} | 无法连接到ADB地址{ADB_address}")
if not self.isInterruptionRequested:
self.play_sound.emit("ADB失败")
def refresh_maa_log(self) -> None:
"""刷新MAA日志"""
@@ -923,6 +1102,8 @@ class MaaManager(QObject):
latest_time = start_time
for _ in logs[::-1]:
try:
if "如果长时间无进一步日志更新,可能需要手动干预。" in _:
continue
latest_time = datetime.strptime(_[1:20], "%Y-%m-%d %H:%M:%S")
break
except ValueError:
@@ -965,7 +1146,7 @@ class MaaManager(QObject):
else:
self.maa_result = "MAA部分任务执行失败"
elif "检查连接设置」或「尝试重启模拟器与 ADB」或「重启电脑" in log:
elif "检查连接设置」 → 「尝试重启模拟器与 ADB」 → 「重启电脑" in log:
self.maa_result = "MAA的ADB连接异常"
elif "未检测到任何模拟器" in log:
@@ -991,7 +1172,7 @@ class MaaManager(QObject):
elif mode == "人工排查":
if "完成任务: StartUp" in log:
self.maa_result = "Success!"
elif "检查连接设置」或「尝试重启模拟器与 ADB」或「重启电脑" in log:
elif "检查连接设置」 → 「尝试重启模拟器与 ADB」 → 「重启电脑" in log:
self.maa_result = "MAA的ADB连接异常"
elif "未检测到任何模拟器" in log:
self.maa_result = "MAA未检测到任何模拟器"
@@ -1100,10 +1281,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" # 时间设置
@@ -1166,6 +1352,9 @@ class MaaManager(QObject):
]["Id"]
# 按预设设定任务
data["Configurations"]["Default"][
"TaskQueue.WakeUp.IsChecked"
] = "True" # 开始唤醒
data["Configurations"]["Default"]["TaskQueue.Recruiting.IsChecked"] = (
self.task_dict["Recruiting"]
) # 自动公招
@@ -1190,10 +1379,6 @@ class MaaManager(QObject):
if user_data["Info"]["Mode"] == "简洁":
data["Configurations"]["Default"][
"TaskQueue.WakeUp.IsChecked"
] = "True" # 开始唤醒
data["Configurations"]["Default"]["Start.ClientType"] = user_data[
"Info"
][
@@ -1292,7 +1477,9 @@ class MaaManager(QObject):
] = "True" # 备选关卡
data["Configurations"]["Default"][
"Fight.UseRemainingSanityStage"
] = "True" # 使用剩余理智
] = (
"True" if user_data["Info"]["GameId_Remain"] != "-" else "False"
) # 使用剩余理智
data["Configurations"]["Default"][
"Fight.UseExpiringMedicine"
] = "True" # 无限吃48小时内过期的理智药
@@ -1390,7 +1577,9 @@ class MaaManager(QObject):
] = "True" # 备选关卡
data["Configurations"]["Default"][
"Fight.UseRemainingSanityStage"
] = "True" # 使用剩余理智
] = (
"True" if user_data["Info"]["GameId_Remain"] != "-" else "False"
) # 使用剩余理智
# 基建模式
if (
@@ -1406,7 +1595,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"][
@@ -1416,12 +1604,20 @@ class MaaManager(QObject):
"Start.RunDirectly"
] = "True" # 启动MAA后直接运行
data["Global"]["Start.MinimizeDirectly"] = "True" # 启动MAA后直接最小化
data["Global"]["GUI.UseTray"] = "True" # 显示托盘图标
data["Global"]["GUI.MinimizeToTray"] = "True" # 最小化时隐藏至托盘
data["Configurations"]["Default"]["Start.OpenEmulatorAfterLaunch"] = str(
self.if_open_emulator
) # 启动MAA后自动开启模拟器
data["Global"][
"VersionUpdate.ScheduledUpdateCheck"
] = "False" # 定时检查更新
data["Global"][
"VersionUpdate.AutoDownloadUpdatePackage"
] = "False" # 自动下载更新包
data["Global"][
"VersionUpdate.AutoInstallUpdatePackage"
] = "False" # 自动安装更新包
# 账号切换
if user_data["Info"]["Server"] == "Official":
@@ -1437,15 +1633,6 @@ class MaaManager(QObject):
if user_data["Info"]["Mode"] == "简洁":
data["Global"][
"VersionUpdate.ScheduledUpdateCheck"
] = "False" # 定时检查更新
data["Global"][
"VersionUpdate.AutoDownloadUpdatePackage"
] = "False" # 自动下载更新包
data["Global"][
"VersionUpdate.AutoInstallUpdatePackage"
] = "False" # 自动安装更新包
data["Configurations"]["Default"]["Start.ClientType"] = user_data[
"Info"
][
@@ -1480,7 +1667,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"][
@@ -1492,6 +1678,15 @@ class MaaManager(QObject):
data["Configurations"]["Default"][
"Start.OpenEmulatorAfterLaunch"
] = "False" # 启动MAA后自动开启模拟器
data["Global"][
"VersionUpdate.ScheduledUpdateCheck"
] = "False" # 定时检查更新
data["Global"][
"VersionUpdate.AutoDownloadUpdatePackage"
] = "False" # 自动下载更新包
data["Global"][
"VersionUpdate.AutoInstallUpdatePackage"
] = "False" # 自动安装更新包
if Config.get(Config.function_IfSilence):
data["Global"][
@@ -1500,15 +1695,6 @@ class MaaManager(QObject):
if "全局" in mode:
data["Global"][
"VersionUpdate.ScheduledUpdateCheck"
] = "False" # 定时检查更新
data["Global"][
"VersionUpdate.AutoDownloadUpdatePackage"
] = "False" # 自动下载更新包
data["Global"][
"VersionUpdate.AutoInstallUpdatePackage"
] = "False" # 自动安装更新包
data["Configurations"]["Default"][
"TaskQueue.WakeUp.IsChecked"
] = "False" # 开始唤醒
@@ -1536,7 +1722,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"][
@@ -1633,16 +1818,21 @@ class MaaManager(QObject):
mode: str,
title: str,
message: Union[str, dict],
if_get_text_only: bool = False,
) -> str:
user_data: Dict[str, Dict[str, Union[str, int, bool]]] = None,
) -> None:
"""通过所有渠道推送通知"""
env = Environment(
loader=FileSystemLoader(str(Config.app_path / "resources/html"))
)
if mode == "代理结果":
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"
@@ -1654,9 +1844,6 @@ class MaaManager(QObject):
if len(message["waiting_user"]) > 0:
message_text += f"\n未开始{self.mode[2:4]}的用户:\n{"\n".join(message["waiting_user"])}\n"
if if_get_text_only:
return message_text
# 生成HTML通知内容
message["failed_user"] = "".join(message["failed_user"])
message["waiting_user"] = "".join(message["waiting_user"])
@@ -1664,11 +1851,31 @@ class MaaManager(QObject):
template = env.get_template("MAA_result.html")
message_html = template.render(message)
Notify.send_mail("网页", title, message_html)
Notify.ServerChanPush(title, f"{message_text}\n\nAUTO_MAA 敬上")
Notify.CompanyWebHookBotPush(title, f"{message_text}\n\nAUTO_MAA 敬上")
# ServerChan的换行是两个换行符。故而将\n替换为\n\n
serverchan_message = message_text.replace("\n", "\n\n")
return message_text
# 发送全局通知
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 == "统计信息":
@@ -1697,18 +1904,165 @@ class MaaManager(QObject):
template = env.get_template("MAA_statistics.html")
message_html = template.render(message)
Notify.send_mail("网页", title, message_html)
# ServerChan的换行是两个换行符。故而将\n替换为\n\n
serverchan_message = message_text.replace("\n", "\n\n")
Notify.ServerChanPush(title, f"{serverchan_message}\n\nAUTO_MAA 敬上")
Notify.CompanyWebHookBotPush(title, f"{message_text}\n\nAUTO_MAA 敬上")
# 发送全局通知
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 (
user_data["Notify"]["Enabled"]
and user_data["Notify"]["IfSendStatistic"]
):
# 发送邮件通知
if user_data["Notify"]["IfSendMail"]:
if user_data["Notify"]["ToAddress"]:
Notify.send_mail(
"网页",
title,
message_html,
user_data["Notify"]["ToAddress"],
)
else:
logger.error(
f"{self.name} | 用户邮箱地址为空,无法发送用户单独的邮件通知"
)
# 发送ServerChan通知
if user_data["Notify"]["IfServerChan"]:
if user_data["Notify"]["ServerChanKey"]:
Notify.ServerChanPush(
title,
f"{serverchan_message}\n\nAUTO_MAA 敬上",
user_data["Notify"]["ServerChanKey"],
user_data["Notify"]["ServerChanTag"],
user_data["Notify"]["ServerChanChannel"],
)
else:
logger.error(
f"{self.name} |用户ServerChan密钥为空无法发送用户单独的ServerChan通知"
)
# 推送CompanyWebHookBot通知
if user_data["Notify"]["IfCompanyWebHookBot"]:
if user_data["Notify"]["CompanyWebHookBotUrl"]:
Notify.CompanyWebHookBotPush(
title,
f"{message_text}\n\nAUTO_MAA 敬上",
user_data["Notify"]["CompanyWebHookBotUrl"],
)
else:
logger.error(
f"{self.name} |用户CompanyWebHookBot密钥为空无法发送用户单独的CompanyWebHookBot通知"
)
elif mode == "公招六星":
# 生成HTML通知内容
template = env.get_template("MAA_six_star.html")
message_html = template.render(message)
Notify.send_mail("网页", title, message_html)
Notify.ServerChanPush(title, "好羡慕~\n\nAUTO_MAA 敬上")
Notify.CompanyWebHookBotPush(title, "好羡慕~\n\nAUTO_MAA 敬上")
# 发送全局通知
if Config.get(Config.notify_IfSendSixStar):
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,
"好羡慕~\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,
"好羡慕~\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"]:
# 发送邮件通知
if user_data["Notify"]["IfSendMail"]:
if user_data["Notify"]["ToAddress"]:
Notify.send_mail(
"网页",
title,
message_html,
user_data["Notify"]["ToAddress"],
)
else:
logger.error(
f"{self.name} | 用户邮箱地址为空,无法发送用户单独的邮件通知"
)
# 发送ServerChan通知
if user_data["Notify"]["IfServerChan"]:
if user_data["Notify"]["ServerChanKey"]:
Notify.ServerChanPush(
title,
"好羡慕~\n\nAUTO_MAA 敬上",
user_data["Notify"]["ServerChanKey"],
user_data["Notify"]["ServerChanTag"],
user_data["Notify"]["ServerChanChannel"],
)
else:
logger.error(
f"{self.name} |用户ServerChan密钥为空无法发送用户单独的ServerChan通知"
)
# 推送CompanyWebHookBot通知
if user_data["Notify"]["IfCompanyWebHookBot"]:
if user_data["Notify"]["CompanyWebHookBotUrl"]:
Notify.CompanyWebHookBotPush(
title,
"好羡慕~\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

@@ -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

@@ -25,23 +25,26 @@ v4.3
作者DLmaster_361
"""
from PySide6.QtWidgets import QWidget
from PySide6.QtCore import Signal
import requests
import time
from loguru import logger
from plyer import notification
import re
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import time
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
from loguru import logger
from plyer import notification
from app.core import Config
from app.services.security import Crypto
from app.utils.ImageUtils import ImageUtils
class Notification(QWidget):
class Notification(QObject):
push_info_bar = Signal(str, str, str, int)
@@ -65,172 +68,231 @@ class Notification(QWidget):
return True
def send_mail(self, mode, title, content) -> None:
def send_mail(self, mode, title, content, to_address) -> None:
"""推送邮件通知"""
if Config.get(Config.notify_IfSendMail):
if (
Config.get(Config.notify_SMTPServerAddress) == ""
or Config.get(Config.notify_AuthorizationCode) == ""
or not bool(
re.match(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
Config.get(Config.notify_FromAddress),
)
if (
Config.get(Config.notify_SMTPServerAddress) == ""
or Config.get(Config.notify_AuthorizationCode) == ""
or not bool(
re.match(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
Config.get(Config.notify_FromAddress),
)
or not bool(
re.match(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
Config.get(Config.notify_ToAddress),
)
)
or not bool(
re.match(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
to_address,
)
):
)
):
logger.error(
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址"
)
self.push_info_bar.emit(
"error",
"邮件通知推送异常",
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址",
-1,
)
return None
try:
# 定义邮件正文
if mode == "文本":
message = MIMEText(content, "plain", "utf-8")
elif mode == "网页":
message = MIMEMultipart("alternative")
message["From"] = formataddr(
(
Header("AUTO_MAA通知服务", "utf-8").encode(),
Config.get(Config.notify_FromAddress),
)
) # 发件人显示的名字
message["To"] = formataddr(
(
Header("AUTO_MAA用户", "utf-8").encode(),
to_address,
)
) # 收件人显示的名字
message["Subject"] = Header(title, "utf-8")
if mode == "网页":
message.attach(MIMEText(content, "html", "utf-8"))
smtpObj = smtplib.SMTP_SSL(
Config.get(Config.notify_SMTPServerAddress),
465,
)
smtpObj.login(
Config.get(Config.notify_FromAddress),
Crypto.win_decryptor(Config.get(Config.notify_AuthorizationCode)),
)
smtpObj.sendmail(
Config.get(Config.notify_FromAddress),
to_address,
message.as_string(),
)
smtpObj.quit()
logger.success("邮件发送成功")
return None
except Exception as e:
logger.error(f"发送邮件时出错:\n{e}")
self.push_info_bar.emit("error", "发送邮件时出错", f"{e}", -1)
return None
return None
def ServerChanPush(self, title, content, send_key, tag, channel):
"""使用Server酱推送通知"""
if not send_key:
logger.error("请正确设置Server酱的SendKey")
self.push_info_bar.emit(
"error", "Server酱通知推送异常", "请正确设置Server酱的SendKey", -1
)
return None
try:
# 构造 URL
if send_key.startswith("sctp"):
match = re.match(r"^sctp(\d+)t", send_key)
if match:
url = f"https://{match.group(1)}.push.ft07.com/send/{send_key}.send"
else:
raise ValueError("SendKey 格式错误sctp")
else:
url = f"https://sctapi.ftqq.com/{send_key}.send"
# 构建 tags 和 channel
def is_valid(s):
return s == "" or (
s == "|".join(s.split("|"))
and (s.count("|") == 0 or all(s.split("|")))
)
tags = "|".join(_.strip() for _ in tag.split("|"))
channels = "|".join(_.strip() for _ in channel.split("|"))
options = {}
if is_valid(tags):
options["tags"] = tags
else:
logger.warning("Server酱 Tag 配置不正确,将被忽略")
self.push_info_bar.emit(
"warning",
"Server酱通知推送异常",
"请正确设置 ServerChan 的 Tag",
-1,
)
if is_valid(channels):
options["channel"] = channels
else:
logger.warning("Server酱 Channel 配置不正确,将被忽略")
self.push_info_bar.emit(
"warning",
"Server酱通知推送异常",
"请正确设置 ServerChan 的 Channel",
-1,
)
# 请求发送
params = {"title": title, "desp": content, **options}
headers = {"Content-Type": "application/json;charset=utf-8"}
response = requests.post(url, json=params, headers=headers, timeout=10)
result = response.json()
if result.get("code") == 0:
logger.info("Server酱推送通知成功")
return True
else:
error_code = result.get("code", "-1")
logger.error(f"Server酱通知推送失败响应码{error_code}")
self.push_info_bar.emit(
"error", "Server酱通知推送失败", f"响应码:{error_code}", -1
)
return f"Server酱通知推送失败{error_code}"
except Exception as e:
logger.exception("Server酱通知推送异常")
self.push_info_bar.emit(
"error",
"Server酱通知推送异常",
"请检查相关设置和网络连接。如全部配置正确,请稍后再试。",
-1,
)
return f"Server酱通知推送异常{str(e)}"
def CompanyWebHookBotPush(self, title, content, webhook_url):
"""使用企业微信群机器人推送通知"""
if webhook_url == "":
logger.error("请正确设置企业微信群机器人的WebHook地址")
self.push_info_bar.emit(
"error",
"企业微信群机器人通知推送异常",
"请正确设置企业微信群机器人的WebHook地址",
-1,
)
return None
content = f"{title}\n{content}"
data = {"msgtype": "text", "text": {"content": content}}
for _ in range(3):
try:
response = requests.post(
url=webhook_url,
json=data,
timeout=10,
)
info = response.json()
break
except Exception as e:
err = e
time.sleep(0.1)
else:
logger.error(f"推送企业微信群机器人时出错:{err}")
self.push_info_bar.emit(
"error",
"企业微信群机器人通知推送失败",
f"使用企业微信群机器人推送通知时出错:{err}",
-1,
)
return None
if info["errcode"] == 0:
logger.info("企业微信群机器人推送通知成功")
return True
else:
logger.error(f"企业微信群机器人推送通知失败:{info}")
self.push_info_bar.emit(
"error",
"企业微信群机器人通知推送失败",
f"使用企业微信群机器人推送通知时出错:{err}",
-1,
)
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(
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址"
"图片推送异常 | 图片不存在或者压缩失败,请检查图片路径是否正确"
)
self.push_info_bar.emit(
"error",
"邮件通知推送异常",
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址",
"企业微信群机器人通知推送异常",
"图片不存在或者压缩失败,请检查图片路径是否正确",
-1,
)
return None
return False
try:
# 定义邮件正文
if mode == "文本":
message = MIMEText(content, "plain", "utf-8")
elif mode == "网页":
message = MIMEMultipart("alternative")
message["From"] = formataddr(
(
Header("AUTO_MAA通知服务", "utf-8").encode(),
Config.get(Config.notify_FromAddress),
)
) # 发件人显示的名字
message["To"] = formataddr(
(
Header("AUTO_MAA用户", "utf-8").encode(),
Config.get(Config.notify_ToAddress),
)
) # 收件人显示的名字
message["Subject"] = Header(title, "utf-8")
if mode == "网页":
message.attach(MIMEText(content, "html", "utf-8"))
smtpObj = smtplib.SMTP_SSL(
Config.get(Config.notify_SMTPServerAddress),
465,
)
smtpObj.login(
Config.get(Config.notify_FromAddress),
Crypto.win_decryptor(Config.get(Config.notify_AuthorizationCode)),
)
smtpObj.sendmail(
Config.get(Config.notify_FromAddress),
Config.get(Config.notify_ToAddress),
message.as_string(),
)
smtpObj.quit()
logger.success("邮件发送成功")
except Exception as e:
logger.error(f"发送邮件时出错:\n{e}")
self.push_info_bar.emit("error", "发送邮件时出错", f"{e}", -1)
def ServerChanPush(self, title, content):
"""使用Server酱推送通知支持 tag 和 channel避免使用SDK"""
if Config.get(Config.notify_IfServerChan):
send_key = Config.get(Config.notify_ServerChanKey)
if not send_key:
logger.error("请正确设置Server酱的SendKey")
self.push_info_bar.emit(
"error", "Server酱通知推送异常", "请正确设置Server酱的SendKey", -1
)
return None
try:
# 构造 URL
if send_key.startswith("sctp"):
match = re.match(r"^sctp(\d+)t", send_key)
if match:
url = f"https://{match.group(1)}.push.ft07.com/send/{send_key}.send"
else:
raise ValueError("SendKey 格式错误sctp")
else:
url = f"https://sctapi.ftqq.com/{send_key}.send"
# 构建 tags 和 channel
def is_valid(s):
return s == "" or (
s == "|".join(s.split("|"))
and (s.count("|") == 0 or all(s.split("|")))
)
tags = "|".join(
_.strip()
for _ in Config.get(Config.notify_ServerChanTag).split("|")
)
channels = "|".join(
_.strip()
for _ in Config.get(Config.notify_ServerChanChannel).split("|")
)
options = {}
if is_valid(tags):
options["tags"] = tags
else:
logger.warning("Server酱 Tag 配置不正确,将被忽略")
self.push_info_bar.emit(
"warning",
"Server酱通知推送异常",
"请正确设置 ServerChan 的 Tag",
-1,
)
if is_valid(channels):
options["channel"] = channels
else:
logger.warning("Server酱 Channel 配置不正确,将被忽略")
self.push_info_bar.emit(
"warning",
"Server酱通知推送异常",
"请正确设置 ServerChan 的 Channel",
-1,
)
# 请求发送
params = {"title": title, "desp": content, **options}
headers = {"Content-Type": "application/json;charset=utf-8"}
response = requests.post(url, json=params, headers=headers, timeout=10)
result = response.json()
if result.get("code") == 0:
logger.info("Server酱推送通知成功")
return True
else:
error_code = result.get("code", "-1")
logger.error(f"Server酱通知推送失败响应码{error_code}")
self.push_info_bar.emit(
"error", "Server酱通知推送失败", f"响应码:{error_code}", -1
)
return f"Server酱通知推送失败{error_code}"
except Exception as e:
logger.exception("Server酱通知推送异常")
self.push_info_bar.emit(
"error", "Server酱通知推送异常", f"请检查相关设置,如还有问题可联系开发者", -1
)
return f"Server酱通知推送异常{str(e)}"
def CompanyWebHookBotPush(self, title, content):
"""使用企业微信群机器人推送通知"""
if Config.get(Config.notify_IfCompanyWebHookBot):
if Config.get(Config.notify_CompanyWebHookBotUrl) == "":
if not webhook_url:
logger.error("请正确设置企业微信群机器人的WebHook地址")
self.push_info_bar.emit(
"error",
@@ -238,45 +300,72 @@ class Notification(QWidget):
"请正确设置企业微信群机器人的WebHook地址",
-1,
)
return None
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.error(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},
}
content = f"{title}\n{content}"
data = {"msgtype": "text", "text": {"content": content}}
# 从远程服务器获取最新主题图像
for _ in range(3):
try:
response = requests.post(
url=Config.get(Config.notify_CompanyWebHookBotUrl),
url=webhook_url,
json=data,
timeout=10,
)
info = response.json()
break
except Exception as e:
except requests.RequestException as e:
err = e
logger.warning(f"推送企业微信群机器人图片第{_+1}次失败:{e}")
time.sleep(0.1)
else:
logger.error(f"推送企业微信群机器人时出错:{err}")
logger.error(f"推送企业微信群机器人图片时出错:{err}")
self.push_info_bar.emit(
"error",
"企业微信群机器人通知推送失败",
f'使用企业微信群机器人推送通知时出错:{info["errmsg"]}',
"企业微信群机器人图片推送失败",
f"使用企业微信群机器人推送图片时出错:{err}",
-1,
)
return None
return False
if info["errcode"] == 0:
logger.info("企业微信群机器人推送通知成功")
if info.get("errcode") == 0:
logger.info("企业微信群机器人推送图片成功")
return True
else:
logger.error(f"企业微信群机器人推送通知失败:{info}")
logger.error(f"企业微信群机器人推送图片失败:{info}")
self.push_info_bar.emit(
"error",
"企业微信群机器人通知推送失败",
f'使用企业微信群机器人推送通知时出错:{info["errmsg"]}',
"企业微信群机器人图片推送失败",
f"使用企业微信群机器人推送图片时出错:{info}",
-1,
)
return f'使用企业微信群机器人推送通知时出错:{info["errmsg"]}'
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):
"""发送测试通知到所有已启用的通知渠道"""
@@ -294,6 +383,7 @@ class Notification(QWidget):
"文本",
"AUTO_MAA测试通知",
"这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
Config.get(Config.notify_ToAddress),
)
# 发送Server酱通知
@@ -301,6 +391,9 @@ class Notification(QWidget):
self.ServerChanPush(
"AUTO_MAA测试通知",
"这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
Config.get(Config.notify_ServerChanKey),
Config.get(Config.notify_ServerChanTag),
Config.get(Config.notify_ServerChanChannel),
)
# 发送企业微信机器人通知
@@ -308,6 +401,11 @@ class Notification(QWidget):
self.CompanyWebHookBotPush(
"AUTO_MAA测试通知",
"这是 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

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

@@ -0,0 +1,241 @@
# 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.3
作者DLmaster_361、ClozyA
"""
from loguru import logger
import time
import json
import hmac
import hashlib
import requests
from urllib import parse
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
).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,
).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
),
).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,
).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

@@ -26,7 +26,7 @@ v4.3
"""
from loguru import logger
from PySide6.QtWidgets import QApplication, QWidget
from PySide6.QtWidgets import QApplication
import sys
import ctypes
import win32gui
@@ -87,7 +87,7 @@ class _SystemHandler:
if sys.platform.startswith("win"):
if mode == "None":
if mode == "NoAction":
logger.info("不执行系统电源操作")
@@ -112,10 +112,11 @@ class _SystemHandler:
Config.main_window.close()
QApplication.quit()
sys.exit(0)
elif sys.platform.startswith("linux"):
if mode == "None":
if mode == "NoAction":
logger.info("不执行系统电源操作")
@@ -138,6 +139,7 @@ class _SystemHandler:
Config.main_window.close()
QApplication.quit()
sys.exit(0)
def is_startup(self) -> bool:
"""判断程序是否已经开机自启"""

File diff suppressed because it is too large Load Diff

View File

@@ -33,8 +33,8 @@ from PySide6.QtWidgets import (
QHBoxLayout,
)
from qfluentwidgets import (
BodyLabel,
CardWidget,
Pivot,
ScrollArea,
FluentIcon,
HeaderCardWidget,
@@ -44,13 +44,12 @@ from qfluentwidgets import (
SubtitleLabel,
PushButton,
)
from PySide6.QtCore import Qt
from PySide6.QtGui import QTextCursor
from typing import List, Dict
from app.core import Config, TaskManager, Task, MainInfoBar
from .Widget import StatefulItemCard, ComboBoxMessageBox
from app.core import Config, TaskManager, Task, MainInfoBar, SoundPlayer
from .Widget import StatefulItemCard, ComboBoxMessageBox, PivotArea
class DispatchCenter(QWidget):
@@ -60,13 +59,29 @@ class DispatchCenter(QWidget):
self.setObjectName("调度中枢")
self.pivot = Pivot(self)
self.multi_button = PushButton(FluentIcon.ADD, "添加任务", self)
self.multi_button.setToolTip("添加任务")
self.multi_button.clicked.connect(self.start_multi_task)
self.power_combox = ComboBox()
self.power_combox.addItem("无动作", userData="NoAction")
self.power_combox.addItem("退出软件", userData="KillSelf")
self.power_combox.addItem("睡眠", userData="Sleep")
self.power_combox.addItem("休眠", userData="Hibernate")
self.power_combox.addItem("关机", userData="Shutdown")
self.power_combox.setCurrentText("无动作")
self.power_combox.currentIndexChanged.connect(self.set_power_sign)
self.pivotArea = PivotArea(self)
self.pivot = self.pivotArea.pivot
self.stackedWidget = QStackedWidget(self)
self.Layout = QVBoxLayout(self)
self.stackedWidget.setContentsMargins(0, 0, 0, 0)
self.stackedWidget.setStyleSheet("background: transparent; border: none;")
self.script_list: Dict[str, DispatchBox] = {}
self.script_list: Dict[str, DispatchCenter.DispatchBox] = {}
dispatch_box = DispatchBox("主调度台", self)
dispatch_box = self.DispatchBox("主调度台", self)
self.script_list["主调度台"] = dispatch_box
self.stackedWidget.addWidget(self.script_list["主调度台"])
self.pivot.addItem(
@@ -76,7 +91,15 @@ class DispatchCenter(QWidget):
icon=FluentIcon.CAFE,
)
self.Layout.addWidget(self.pivot, 0, Qt.AlignHCenter)
h_layout = QHBoxLayout()
h_layout.addWidget(self.multi_button)
h_layout.addWidget(self.pivotArea)
h_layout.addWidget(BodyLabel("全部完成后", self))
h_layout.addWidget(self.power_combox)
h_layout.setContentsMargins(11, 5, 11, 0)
self.Layout = QVBoxLayout(self)
self.Layout.addLayout(h_layout)
self.Layout.addWidget(self.stackedWidget)
self.Layout.setContentsMargins(0, 0, 0, 0)
@@ -87,7 +110,7 @@ class DispatchCenter(QWidget):
def add_board(self, task: Task) -> None:
"""添加一个调度台界面"""
dispatch_box = DispatchBox(task.name, self)
dispatch_box = self.DispatchBox(task.name, self)
dispatch_box.top_bar.main_button.clicked.connect(
lambda: TaskManager.stop_task(task.name)
@@ -123,7 +146,6 @@ class DispatchCenter(QWidget):
self.script_list["主调度台"].top_bar.Lable.show()
self.script_list["主调度台"].top_bar.object.hide()
self.script_list["主调度台"].top_bar.mode.hide()
self.script_list["主调度台"].top_bar.multi_button.show()
self.script_list["主调度台"].top_bar.main_button.clicked.disconnect()
self.script_list["主调度台"].top_bar.main_button.setText("中止任务")
self.script_list["主调度台"].top_bar.main_button.clicked.connect(
@@ -154,7 +176,6 @@ class DispatchCenter(QWidget):
self.script_list["主调度台"].top_bar.Lable.hide()
self.script_list["主调度台"].top_bar.object.show()
self.script_list["主调度台"].top_bar.mode.show()
self.script_list["主调度台"].top_bar.multi_button.hide()
self.script_list["主调度台"].top_bar.main_button.clicked.disconnect()
self.script_list["主调度台"].top_bar.main_button.setText("开始任务")
self.script_list["主调度台"].top_bar.main_button.clicked.connect(
@@ -208,301 +229,323 @@ class DispatchCenter(QWidget):
self.script_list["主调度台"].top_bar.mode.addItems(["自动代理", "人工排查"])
self.script_list["主调度台"].top_bar.mode.setCurrentIndex(0)
def update_power_sign(self) -> None:
"""更新电源设置"""
class DispatchBox(QWidget):
mode_book = {
"NoAction": "无动作",
"KillSelf": "退出软件",
"Sleep": "睡眠",
"Hibernate": "休眠",
"Shutdown": "关机",
}
self.power_combox.currentIndexChanged.disconnect()
self.power_combox.setCurrentText(mode_book[Config.power_sign])
self.power_combox.currentIndexChanged.connect(self.set_power_sign)
def __init__(self, name: str, parent=None):
super().__init__(parent)
def set_power_sign(self) -> None:
"""设置所有任务完成后动作"""
self.setObjectName(name)
if not Config.running_list:
layout = QVBoxLayout()
scrollArea = ScrollArea()
scrollArea.setWidgetResizable(True)
content_widget = QWidget()
content_layout = QVBoxLayout(content_widget)
self.top_bar = self.DispatchTopBar(self, name)
self.info = self.DispatchInfoCard(self)
content_layout.addWidget(self.top_bar)
content_layout.addWidget(self.info)
scrollArea.setWidget(content_widget)
layout.addWidget(scrollArea)
self.setLayout(layout)
class DispatchTopBar(CardWidget):
def __init__(self, parent=None, name: str = None):
super().__init__(parent)
Layout = QHBoxLayout(self)
if name == "主调度台":
self.Lable = SubtitleLabel("", self)
self.Lable.hide()
self.object = ComboBox()
self.object.setPlaceholderText("请选择调度对象")
self.mode = ComboBox()
self.mode.setPlaceholderText("请选择调度模式")
self.multi_button = PushButton("添加任务")
self.multi_button.clicked.connect(self.start_multi_task)
self.main_button = PushButton("开始任务")
self.main_button.clicked.connect(self.start_main_task)
self.multi_button.hide()
Layout.addWidget(self.Lable)
Layout.addWidget(self.object)
Layout.addWidget(self.mode)
Layout.addStretch(1)
Layout.addWidget(self.multi_button)
Layout.addWidget(self.main_button)
else:
self.Lable = SubtitleLabel(name, self)
self.main_button = PushButton("中止任务")
Layout.addWidget(self.Lable)
Layout.addStretch(1)
Layout.addWidget(self.main_button)
def start_main_task(self):
"""开始任务"""
if self.object.currentIndex() == -1:
logger.warning("未选择调度对象")
MainInfoBar.push_info_bar(
"warning", "未选择调度对象", "请选择后再开始任务", 5000
)
return None
if self.mode.currentIndex() == -1:
logger.warning("未选择调度模式")
MainInfoBar.push_info_bar(
"warning", "未选择调度模式", "请选择后再开始任务", 5000
)
return None
if self.object.currentData() in Config.running_list:
logger.warning(f"任务已存在:{self.object.currentData()}")
MainInfoBar.push_info_bar(
"warning", "任务已存在", self.object.currentData(), 5000
)
return None
if "调度队列" in self.object.currentData():
logger.info(f"用户添加任务:{self.object.currentData()}")
TaskManager.add_task(
f"{self.mode.currentText()}_主调度台",
self.object.currentData(),
Config.queue_dict[self.object.currentData()]["Config"].toDict(),
)
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()}},
)
def start_multi_task(self):
"""开始任务"""
# 获取所有可用的队列和实例
text_list = []
data_list = []
for name, info in Config.queue_dict.items():
if name in Config.running_list:
continue
text_list.append(
"队列"
if info["Config"].get(info["Config"].queueSet_Name) == ""
else f"队列 - {info["Config"].get(info["Config"].queueSet_Name)}"
)
data_list.append(name)
for name, info in Config.member_dict.items():
if name in Config.running_list:
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)}"
)
data_list.append(name)
choice = ComboBoxMessageBox(
self.window(),
"选择一个对象以添加相应多开任务",
["选择调度对象"],
[text_list],
[data_list],
self.power_combox.currentIndexChanged.disconnect()
self.power_combox.setCurrentText("无动作")
self.power_combox.currentIndexChanged.connect(self.set_power_sign)
logger.warning("没有正在运行的任务,无法设置任务完成后动作")
MainInfoBar.push_info_bar(
"warning",
"没有正在运行的任务",
"无法设置任务完成后动作",
5000,
)
if choice.exec() and choice.input[0].currentIndex() != -1:
else:
if choice.input[0].currentData() in Config.running_list:
logger.warning(f"任务已存在:{choice.input[0].currentData()}")
MainInfoBar.push_info_bar(
"warning", "任务已存在", choice.input[0].currentData(), 5000
)
return None
Config.set_power_sign(self.power_combox.currentData())
if "调度队列" in choice.input[0].currentData():
def start_multi_task(self) -> None:
"""开始任务"""
# 获取所有可用的队列和实例
text_list = []
data_list = []
for name, info in Config.queue_dict.items():
if name in Config.running_list:
continue
text_list.append(
"队列"
if info["Config"].get(info["Config"].queueSet_Name) == ""
else f"队列 - {info["Config"].get(info["Config"].queueSet_Name)}"
)
data_list.append(name)
for name, info in Config.member_dict.items():
if name in Config.running_list:
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)}"
)
data_list.append(name)
choice = ComboBoxMessageBox(
self.window(),
"选择一个对象以添加相应多开任务",
["选择调度对象"],
[text_list],
[data_list],
)
if choice.exec() and choice.input[0].currentIndex() != -1:
if choice.input[0].currentData() in Config.running_list:
logger.warning(f"任务已存在:{choice.input[0].currentData()}")
MainInfoBar.push_info_bar(
"warning", "任务已存在", choice.input[0].currentData(), 5000
)
return None
if "调度队列" in choice.input[0].currentData():
logger.info(f"用户添加任务:{choice.input[0].currentData()}")
TaskManager.add_task(
"自动代理_新调度台",
choice.input[0].currentData(),
Config.queue_dict[choice.input[0].currentData()]["Config"].toDict(),
)
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(
"自动代理_新调度台",
choice.input[0].currentData(),
Config.queue_dict[choice.input[0].currentData()][
"Config"
].toDict(),
f"自定义队列 - {choice.input[0].currentData()}",
{"Queue": {"Member_1": choice.input[0].currentData()}},
)
elif "脚本" in choice.input[0].currentData():
class DispatchBox(QWidget):
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()}},
)
class DispatchInfoCard(HeaderCardWidget):
def __init__(self, parent=None):
def __init__(self, name: str, parent=None):
super().__init__(parent)
self.setTitle("调度信息")
self.setObjectName(name)
self.task = self.TaskInfoCard(self)
self.user = self.UserInfoCard(self)
self.log_text = self.LogCard(self)
self.top_bar = self.DispatchTopBar(self, name)
self.info = self.DispatchInfoCard(self)
self.viewLayout.addWidget(self.task)
self.viewLayout.addWidget(self.user)
self.viewLayout.addWidget(self.log_text)
content_widget = QWidget()
content_layout = QVBoxLayout(content_widget)
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.addWidget(self.top_bar)
content_layout.addWidget(self.info)
self.viewLayout.setStretch(0, 1)
self.viewLayout.setStretch(1, 1)
self.viewLayout.setStretch(2, 5)
scrollArea = ScrollArea()
scrollArea.setWidgetResizable(True)
scrollArea.setContentsMargins(0, 0, 0, 0)
scrollArea.setStyleSheet("background: transparent; border: none;")
scrollArea.setWidget(content_widget)
def update_board(self, task_list: list, user_list: list, log: str):
"""更新调度信息"""
layout = QVBoxLayout(self)
layout.addWidget(scrollArea)
self.task.update_task(task_list)
self.user.update_user(user_list)
self.log_text.text.setText(log)
class DispatchTopBar(CardWidget):
class TaskInfoCard(HeaderCardWidget):
def __init__(self, parent=None, name: str = None):
super().__init__(parent)
Layout = QHBoxLayout(self)
if name == "主调度台":
self.Lable = SubtitleLabel("", self)
self.Lable.hide()
self.object = ComboBox()
self.object.setPlaceholderText("请选择调度对象")
self.mode = ComboBox()
self.mode.setPlaceholderText("请选择调度模式")
self.main_button = PushButton("开始任务")
self.main_button.clicked.connect(self.start_main_task)
Layout.addWidget(self.Lable)
Layout.addWidget(self.object)
Layout.addWidget(self.mode)
Layout.addStretch(1)
Layout.addWidget(self.main_button)
else:
self.Lable = SubtitleLabel(name, self)
self.main_button = PushButton("中止任务")
Layout.addWidget(self.Lable)
Layout.addStretch(1)
Layout.addWidget(self.main_button)
def start_main_task(self):
"""开始任务"""
if self.object.currentIndex() == -1:
logger.warning("未选择调度对象")
MainInfoBar.push_info_bar(
"warning", "未选择调度对象", "请选择后再开始任务", 5000
)
return None
if self.mode.currentIndex() == -1:
logger.warning("未选择调度模式")
MainInfoBar.push_info_bar(
"warning", "未选择调度模式", "请选择后再开始任务", 5000
)
return None
if self.object.currentData() in Config.running_list:
logger.warning(f"任务已存在:{self.object.currentData()}")
MainInfoBar.push_info_bar(
"warning", "任务已存在", self.object.currentData(), 5000
)
return None
if "调度队列" in self.object.currentData():
logger.info(f"用户添加任务:{self.object.currentData()}")
TaskManager.add_task(
f"{self.mode.currentText()}_主调度台",
self.object.currentData(),
Config.queue_dict[self.object.currentData()]["Config"].toDict(),
)
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()}},
)
class DispatchInfoCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("任务队列")
self.Layout = QVBoxLayout()
self.viewLayout.addLayout(self.Layout)
self.viewLayout.setContentsMargins(3, 0, 3, 3)
self.setTitle("调度信息")
self.task_cards: List[StatefulItemCard] = []
self.task = self.TaskInfoCard(self)
self.user = self.UserInfoCard(self)
self.log_text = self.LogCard(self)
def create_task(self, task_list: list):
"""创建任务队列"""
self.viewLayout.addWidget(self.task)
self.viewLayout.addWidget(self.user)
self.viewLayout.addWidget(self.log_text)
while self.Layout.count() > 0:
item = self.Layout.takeAt(0)
if item.spacerItem():
self.Layout.removeItem(item.spacerItem())
elif item.widget():
item.widget().deleteLater()
self.viewLayout.setStretch(0, 1)
self.viewLayout.setStretch(1, 1)
self.viewLayout.setStretch(2, 5)
self.task_cards = []
def update_board(self, task_list: list, user_list: list, log: str):
"""更新调度信息"""
for task in task_list:
self.task.update_task(task_list)
self.user.update_user(user_list)
self.log_text.text.setText(log)
self.task_cards.append(StatefulItemCard(task))
self.Layout.addWidget(self.task_cards[-1])
class TaskInfoCard(HeaderCardWidget):
self.Layout.addStretch(1)
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("任务队列")
def update_task(self, task_list: list):
"""更新任务队列"""
self.Layout = QVBoxLayout()
self.viewLayout.addLayout(self.Layout)
self.viewLayout.setContentsMargins(3, 0, 3, 3)
for i in range(len(task_list)):
self.task_cards: List[StatefulItemCard] = []
self.task_cards[i].update_status(task_list[i][1])
def create_task(self, task_list: list):
"""创建任务队列"""
class UserInfoCard(HeaderCardWidget):
while self.Layout.count() > 0:
item = self.Layout.takeAt(0)
if item.spacerItem():
self.Layout.removeItem(item.spacerItem())
elif item.widget():
item.widget().deleteLater()
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("用户队列")
self.task_cards = []
self.Layout = QVBoxLayout()
self.viewLayout.addLayout(self.Layout)
self.viewLayout.setContentsMargins(3, 0, 3, 3)
for task in task_list:
self.user_cards: List[StatefulItemCard] = []
self.task_cards.append(StatefulItemCard(task))
self.Layout.addWidget(self.task_cards[-1])
def create_user(self, user_list: list):
"""创建用户队列"""
self.Layout.addStretch(1)
while self.Layout.count() > 0:
item = self.Layout.takeAt(0)
if item.spacerItem():
self.Layout.removeItem(item.spacerItem())
elif item.widget():
item.widget().deleteLater()
def update_task(self, task_list: list):
"""更新任务队列"""
self.user_cards = []
for i in range(len(task_list)):
for user in user_list:
self.task_cards[i].update_status(task_list[i][1])
self.user_cards.append(StatefulItemCard(user))
self.Layout.addWidget(self.user_cards[-1])
class UserInfoCard(HeaderCardWidget):
self.Layout.addStretch(1)
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("用户队列")
def update_user(self, user_list: list):
"""更新用户队列"""
self.Layout = QVBoxLayout()
self.viewLayout.addLayout(self.Layout)
self.viewLayout.setContentsMargins(3, 0, 3, 3)
for i in range(len(user_list)):
self.user_cards: List[StatefulItemCard] = []
self.user_cards[i].Label.setText(user_list[i][0])
self.user_cards[i].update_status(user_list[i][1])
def create_user(self, user_list: list):
"""创建用户队列"""
class LogCard(HeaderCardWidget):
while self.Layout.count() > 0:
item = self.Layout.takeAt(0)
if item.spacerItem():
self.Layout.removeItem(item.spacerItem())
elif item.widget():
item.widget().deleteLater()
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("日志")
self.user_cards = []
self.text = TextBrowser()
self.viewLayout.setContentsMargins(3, 0, 3, 3)
self.viewLayout.addWidget(self.text)
for user in user_list:
self.text.textChanged.connect(self.to_end)
self.user_cards.append(StatefulItemCard(user))
self.Layout.addWidget(self.user_cards[-1])
def to_end(self):
"""滚动到底部"""
self.Layout.addStretch(1)
self.text.moveCursor(QTextCursor.End)
self.text.ensureCursorVisible()
def update_user(self, user_list: list):
"""更新用户队列"""
for i in range(len(user_list)):
self.user_cards[i].Label.setText(user_list[i][0])
self.user_cards[i].update_status(user_list[i][1])
class LogCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("日志")
self.text = TextBrowser()
self.viewLayout.setContentsMargins(3, 0, 3, 3)
self.viewLayout.addWidget(self.text)
self.text.textChanged.connect(self.to_end)
def to_end(self):
"""滚动到底部"""
self.text.moveCursor(QTextCursor.End)
self.text.ensureCursorVisible()

View File

@@ -21,24 +21,20 @@
"""
AUTO_MAA
AUTO_MAA更新器
v1.2
v4.3
作者DLmaster_361
"""
import sys
import json
from loguru import logger
import zipfile
import requests
import subprocess
import time
import psutil
import win32crypt
import base64
from packaging import version
from functools import partial
from pathlib import Path
from PySide6.QtWidgets import QApplication, QDialog, QVBoxLayout
from PySide6.QtWidgets import QDialog, QVBoxLayout
from qfluentwidgets import (
ProgressBar,
IndeterminateProgressBar,
@@ -46,7 +42,7 @@ from qfluentwidgets import (
setTheme,
Theme,
)
from PySide6.QtGui import QIcon, QCloseEvent
from PySide6.QtGui import QCloseEvent
from PySide6.QtCore import QThread, Signal, QTimer, QEventLoop
from typing import List, Dict, Union
@@ -83,12 +79,15 @@ 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:
# 清理可能存在的临时文件
@@ -162,10 +161,13 @@ 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:
@@ -237,14 +239,10 @@ class DownloadManager(QDialog):
self.version = version
self.config = config
self.download_path = app_path / "DOWNLOAD_TEMP.zip" # 临时下载文件的路径
self.version_path = app_path / "resources/version.json"
self.download_process_dict: Dict[str, DownloadProcess] = {}
self.timer_dict: Dict[str, QTimer] = {}
self.if_speed_test_accomplish = False
self.setWindowTitle("AUTO_MAA更新器")
self.setWindowIcon(
QIcon(str(app_path / "resources/icons/AUTO_MAA_Updater.ico"))
)
self.resize(700, 70)
setTheme(Theme.AUTO, lazy=True)
@@ -415,6 +413,15 @@ 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
@@ -539,41 +546,25 @@ class DownloadManager(QDialog):
self.zip_extract.start()
self.zip_loop.exec()
self.update_info("正在删除已弃用的文件")
if (self.app_path / "changes.json").exists():
with (self.app_path / "changes.json").open(mode="r", encoding="utf-8") as f:
info: Dict[str, List[str]] = json.load(f)
if "deleted" in info:
for file_path in info["deleted"]:
if (self.app_path / file_path).exists():
(self.app_path / file_path).unlink()
(self.app_path / "changes.json").unlink()
self.update_info("正在删除临时文件")
self.update_progress(0, 0, 0)
if (self.app_path / "changes.json").exists():
(self.app_path / "changes.json").unlink()
if self.download_path.exists():
self.download_path.unlink()
# 主程序更新完成后打开对应程序
if not self.isInterruptionRequested and self.name == "AUTO_MAA":
subprocess.Popen(
[self.app_path / "AUTO_MAA.exe"],
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
| subprocess.DETACHED_PROCESS
| subprocess.CREATE_NO_WINDOW,
)
elif not self.isInterruptionRequested and self.name == "MAA":
# 下载完成后打开对应程序
if not self.isInterruptionRequested and self.name == "MAA":
subprocess.Popen(
[self.app_path / "MAA.exe"],
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
| subprocess.DETACHED_PROCESS
| subprocess.CREATE_NO_WINDOW,
)
self.update_info(f"{self.name}更新成功!")
if self.name == "AUTO_MAA":
self.update_info(f"即将安装{self.name}")
else:
self.update_info(f"{self.name}下载成功!")
self.update_progress(0, 100, 100)
self.download_accomplish.emit()
@@ -615,142 +606,3 @@ class DownloadManager(QDialog):
self.requestInterruption()
event.accept()
class AUTO_MAA_Downloader(QApplication):
def __init__(
self, app_path: Path, name: str, main_version: list, config: dict
) -> None:
super().__init__()
self.main = DownloadManager(app_path, name, main_version, config)
self.main.show()
self.main.run()
if __name__ == "__main__":
# 获取软件自身的路径
app_path = Path(sys.argv[0]).resolve().parent
# 从本地版本信息文件获取当前版本信息
if (app_path / "resources/version.json").exists():
with (app_path / "resources/version.json").open(
mode="r", encoding="utf-8"
) as f:
current_version_info = json.load(f)
current_version = list(
map(int, current_version_info["main_version"].split("."))
)
else:
current_version = [0, 0, 0, 0]
# 从本地配置文件获取更新信息
if (app_path / "config/config.json").exists():
with (app_path / "config/config.json").open(mode="r", encoding="utf-8") as f:
config = json.load(f)
if "Update" in config:
if "UpdateType" in config["Update"]:
update_type = config["Update"]["UpdateType"]
else:
update_type = "stable"
if "ProxyUrlList" in config["Update"]:
proxy_list = config["Update"]["ProxyUrlList"]
else:
proxy_list = []
if "ThreadNumb" in config["Update"]:
thread_numb = config["Update"]["ThreadNumb"]
else:
thread_numb = 8
if "MirrorChyanCDK" in config["Update"]:
mirrorchyan_CDK = (
win32crypt.CryptUnprotectData(
base64.b64decode(config["Update"]["MirrorChyanCDK"]),
None,
None,
None,
0,
)[1].decode("utf-8")
if config["Update"]["MirrorChyanCDK"]
else ""
)
else:
mirrorchyan_CDK = ""
else:
update_type = "stable"
proxy_list = []
thread_numb = 8
mirrorchyan_CDK = ""
else:
update_type = "stable"
proxy_list = []
thread_numb = 8
mirrorchyan_CDK = ""
# 从远程服务器获取最新版本信息
for _ in range(3):
try:
response = requests.get(
f"https://mirrorchyan.com/api/resources/AUTO_MAA/latest?user_agent=AutoMaaDownloader&current_version={version_text(current_version)}&cdk={mirrorchyan_CDK}&channel={update_type}",
timeout=10,
)
version_info: Dict[str, Union[int, str, Dict[str, str]]] = response.json()
break
except Exception as e:
err = e
time.sleep(0.1)
else:
sys.exit(f"获取版本信息时出错:\n{err}")
if version_info["code"] == 0:
if "url" in version_info["data"]:
download_config = {
"mode": "MirrorChyan",
"thread_numb": 1,
"url": version_info["data"]["url"],
}
else:
download_config = {"mode": "Proxy", "thread_numb": thread_numb}
else:
sys.exit(f"获取版本信息时出错:{version_info["msg"]}")
remote_version = list(
map(
int,
version_info["data"]["version_name"][1:].replace("-beta", "").split("."),
)
)
if download_config["mode"] == "Proxy":
for _ in range(3):
try:
response = requests.get(
"https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/download_info.json",
timeout=10,
)
download_info = response.json()
download_config["proxy_list"] = list(
set(proxy_list + download_info["proxy_list"])
)
download_config["download_dict"] = download_info["download_dict"]
break
except Exception as e:
err = e
time.sleep(0.1)
else:
sys.exit(f"获取代理信息时出错:{err}")
if (app_path / "changes.json").exists():
(app_path / "changes.json").unlink()
# 启动更新线程
if version.parse(version_text(remote_version)) > version.parse(
version_text(current_version)
):
app = AUTO_MAA_Downloader(app_path, "AUTO_MAA", remote_version, download_config)
sys.exit(app.exec())

View File

@@ -51,7 +51,7 @@ from pathlib import Path
from typing import Union, List, Dict
from app.core import Config
from app.core import Config, SoundPlayer
from .Widget import StatefulItemCard, QuantifiedItemCard, QuickExpandGroupCard
@@ -61,25 +61,30 @@ class History(QWidget):
super().__init__(parent)
self.setObjectName("历史记录")
self.history_top_bar = self.HistoryTopBar(self)
self.history_top_bar.search_history.connect(self.reload_history)
content_widget = QWidget()
self.content_layout = QVBoxLayout(content_widget)
self.history_top_bar = self.HistoryTopBar(self)
self.history_top_bar.search_history.connect(self.reload_history)
self.content_layout.setContentsMargins(0, 0, 11, 0)
scrollArea = ScrollArea()
scrollArea.setWidgetResizable(True)
scrollArea.setContentsMargins(0, 0, 0, 0)
scrollArea.setStyleSheet("background: transparent; border: none;")
scrollArea.setWidget(content_widget)
layout = QVBoxLayout()
layout = QVBoxLayout(self)
layout.addWidget(self.history_top_bar)
layout.addWidget(scrollArea)
self.setLayout(layout)
self.history_card_list = []
def reload_history(self, mode: str, start_date: QDate, end_date: QDate) -> None:
"""加载历史记录界面"""
SoundPlayer.play("历史记录查询")
while self.content_layout.count() > 0:
item = self.content_layout.takeAt(0)
if item.spacerItem():

View File

@@ -62,14 +62,6 @@ class Home(QWidget):
self.banner = Banner()
self.banner_text = TextBrowser()
widget = QWidget()
Layout = QVBoxLayout(widget)
Layout.addWidget(self.banner)
Layout.addWidget(self.banner_text)
Layout.setStretch(0, 2)
Layout.setStretch(1, 3)
v_layout = QVBoxLayout(self.banner)
v_layout.setContentsMargins(0, 0, 0, 15)
v_layout.setSpacing(5)
@@ -146,12 +138,22 @@ class Home(QWidget):
# 将底部水平布局添加到垂直布局
v_layout.addLayout(h2_layout)
layout = QVBoxLayout()
content_widget = QWidget()
content_layout = QVBoxLayout(content_widget)
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.addWidget(self.banner)
content_layout.addWidget(self.banner_text)
content_layout.setStretch(0, 2)
content_layout.setStretch(1, 3)
scrollArea = ScrollArea()
scrollArea.setWidgetResizable(True)
scrollArea.setWidget(widget)
scrollArea.setContentsMargins(0, 0, 0, 0)
scrollArea.setStyleSheet("background: transparent; border: none;")
scrollArea.setWidget(content_widget)
layout = QVBoxLayout(self)
layout.addWidget(scrollArea)
self.setLayout(layout)
self.set_banner()
@@ -197,20 +199,22 @@ class Home(QWidget):
elif Config.get(Config.function_HomeImageMode) == "主题图像":
# 从远程服务器获取最新主题图像
Network.set_info(
network = Network.add_task(
mode="get",
url="https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/theme_image.json",
)
Network.start()
Network.loop.exec()
if Network.stutus_code == 200:
theme_image = Network.response_json
network.loop.exec()
network_result = Network.get_result(network)
if network_result["status_code"] == 200:
theme_image = network_result["response_json"]
else:
logger.warning(f"获取最新主题图像时出错:{Network.error_message}")
logger.warning(
f"获取最新主题图像时出错:{network_result['error_message']}"
)
MainInfoBar.push_info_bar(
"warning",
"获取最新主题图像时出错",
f"网络错误:{Network.stutus_code}",
f"网络错误:{network_result['status_code']}",
5000,
)
return None
@@ -234,15 +238,15 @@ class Home(QWidget):
> time_local
):
Network.set_info(
network = Network.add_task(
mode="get_file",
url=theme_image["url"],
path=Config.app_path / "resources/images/Home/BannerTheme.jpg",
)
Network.start()
Network.loop.exec()
network.loop.exec()
network_result = Network.get_result(network)
if Network.stutus_code == 200:
if network_result["status_code"] == 200:
with (Config.app_path / "resources/theme_image.json").open(
mode="w", encoding="utf-8"
@@ -259,11 +263,13 @@ class Home(QWidget):
else:
logger.warning(f"下载最新主题图像时出错:{Network.error_message}")
logger.warning(
f"下载最新主题图像时出错:{network_result['error_message']}"
)
MainInfoBar.push_info_bar(
"warning",
"下载最新主题图像时出错",
f"网络错误:{Network.stutus_code}",
f"网络错误:{network_result['status_code']}",
5000,
)
@@ -399,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

@@ -28,7 +28,6 @@ v4.3
from loguru import logger
from PySide6.QtWidgets import QApplication, QSystemTrayIcon
from qfluentwidgets import (
qconfig,
Action,
SystemTrayMenu,
SplashScreen,
@@ -46,10 +45,11 @@ from datetime import datetime, timedelta
import shutil
import darkdetect
from app.core import Config, TaskManager, MainTimer, MainInfoBar
from app.core import Config, TaskManager, MainTimer, MainInfoBar, SoundPlayer
from app.services import Notify, Crypto, System
from .home import Home
from .member_manager import MemberManager
from .plan_manager import PlanManager
from .queue_manager import QueueManager
from .dispatch_center import DispatchCenter
from .history import History
@@ -81,6 +81,7 @@ class AUTO_MAA(MSFluentWindow):
# 创建主窗口
self.home = Home(self)
self.plan_manager = PlanManager(self)
self.member_manager = MemberManager(self)
self.queue_manager = QueueManager(self)
self.dispatch_center = DispatchCenter(self)
@@ -101,6 +102,13 @@ class AUTO_MAA(MSFluentWindow):
FluentIcon.ROBOT,
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.plan_manager,
FluentIcon.CALENDAR,
"计划管理",
FluentIcon.CALENDAR,
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.queue_manager,
FluentIcon.BOOK_SHELF,
@@ -129,23 +137,7 @@ class AUTO_MAA(MSFluentWindow):
FluentIcon.SETTING,
NavigationItemPosition.BOTTOM,
)
self.stackedWidget.currentChanged.connect(
lambda index: (
self.queue_manager.reload_member_name() if index == 2 else None
)
)
self.stackedWidget.currentChanged.connect(
lambda index: (
self.dispatch_center.pivot.setCurrentItem("主调度台")
if index == 3
else None
)
)
self.stackedWidget.currentChanged.connect(
lambda index: (
self.dispatch_center.update_top_bar() if index == 3 else None
)
)
self.stackedWidget.currentChanged.connect(self.__currentChanged)
# 创建系统托盘及其菜单
self.tray = QSystemTrayIcon(
@@ -182,7 +174,7 @@ class AUTO_MAA(MSFluentWindow):
Action(
FluentIcon.POWER_BUTTON,
"退出主程序",
triggered=lambda: (self.window().close(), QApplication.quit()),
triggered=lambda: System.set_power("KillSelf"),
)
)
@@ -193,6 +185,7 @@ class AUTO_MAA(MSFluentWindow):
self.set_min_method()
Config.user_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)
Notify.push_info_bar.connect(MainInfoBar.push_info_bar)
@@ -241,16 +234,127 @@ class AUTO_MAA(MSFluentWindow):
else:
self.setStyleSheet("background-color: #ffffff;")
def set_min_method(self) -> None:
"""设置最小化方法"""
if Config.get(Config.ui_IfToTray):
self.titleBar.minBtn.clicked.disconnect()
self.titleBar.minBtn.clicked.connect(lambda: self.show_ui("隐藏到托盘"))
else:
self.titleBar.minBtn.clicked.disconnect()
self.titleBar.minBtn.clicked.connect(self.window().showMinimized)
def on_tray_activated(self, reason):
"""双击返回主界面"""
if reason == QSystemTrayIcon.DoubleClick:
self.show_ui("显示主窗口")
def show_ui(
self, mode: str, if_quick: bool = False, if_start: bool = False
) -> None:
"""配置窗口状态"""
if Config.args.mode != "gui":
return None
self.switch_theme()
if mode == "显示主窗口":
# 配置主窗口
if not self.window().isVisible():
size = list(
map(
int,
Config.get(Config.ui_size).split("x"),
)
)
location = list(
map(
int,
Config.get(Config.ui_location).split("x"),
)
)
if self.window().isMaximized():
self.window().showNormal()
self.window().setGeometry(location[0], location[1], size[0], size[1])
self.window().show()
if not if_quick:
if (
Config.get(Config.ui_maximized)
and not self.window().isMaximized()
):
self.titleBar.maxBtn.click()
SoundPlayer.play("欢迎回来")
self.show_ui("配置托盘")
elif if_start:
if Config.get(Config.ui_maximized) and not self.window().isMaximized():
self.titleBar.maxBtn.click()
self.show_ui("配置托盘")
# 如果窗口不在屏幕内,则重置窗口位置
if not any(
self.window().geometry().intersects(screen.availableGeometry())
for screen in QApplication.screens()
):
self.window().showNormal()
self.window().setGeometry(100, 100, 1200, 700)
self.window().raise_()
self.window().activateWindow()
while Config.info_bar_list:
info_bar_item = Config.info_bar_list.pop(0)
MainInfoBar.push_info_bar(
info_bar_item["mode"],
info_bar_item["title"],
info_bar_item["content"],
info_bar_item["time"],
)
elif mode == "配置托盘":
if Config.get(Config.ui_IfShowTray):
self.tray.show()
else:
self.tray.hide()
elif mode == "隐藏到托盘":
# 保存窗口相关属性
if not self.window().isMaximized():
Config.set(
Config.ui_size,
f"{self.geometry().width()}x{self.geometry().height()}",
)
Config.set(
Config.ui_location,
f"{self.geometry().x()}x{self.geometry().y()}",
)
Config.set(Config.ui_maximized, self.window().isMaximized())
Config.save()
# 隐藏主窗口
if not if_quick:
self.window().hide()
self.tray.show()
def start_up_task(self) -> None:
"""启动时任务"""
# 清理旧日志
self.clean_old_logs()
# 清理临时更新器
if (Config.app_path / "AUTO_Updater.active.exe").exists():
# 清理安装包
if (Config.app_path / "AUTO_MAA-Setup.exe").exists():
try:
(Config.app_path / "AUTO_Updater.active.exe").unlink()
(Config.app_path / "AUTO_MAA-Setup.exe").unlink()
except Exception:
pass
@@ -278,23 +382,40 @@ class AUTO_MAA(MSFluentWindow):
self.titleBar.minBtn.click()
def set_min_method(self) -> None:
"""设置最小化方法"""
if Config.args.config:
if Config.get(Config.ui_IfToTray):
for config in [_ for _ in Config.args.config if _ in Config.queue_dict]:
self.titleBar.minBtn.clicked.disconnect()
self.titleBar.minBtn.clicked.connect(lambda: self.show_ui("隐藏到托盘"))
TaskManager.add_task(
"自动代理_新调度台",
config,
Config.queue_dict["调度队列_1"]["Config"].toDict(),
)
else:
for config in [_ for _ in Config.args.config if _ in Config.member_dict]:
self.titleBar.minBtn.clicked.disconnect()
self.titleBar.minBtn.clicked.connect(self.window().showMinimized)
TaskManager.add_task(
"自动代理_新调度台",
"自定义队列",
{"Queue": {"Member_1": config}},
)
def on_tray_activated(self, reason):
"""双击返回主界面"""
if reason == QSystemTrayIcon.DoubleClick:
self.show_ui("显示主窗口")
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):
"""
@@ -351,75 +472,16 @@ class AUTO_MAA(MSFluentWindow):
"warning", "启动主任务失败", "“调度队列_1”与“脚本_1”均不存在", -1
)
def show_ui(self, mode: str, if_quick: bool = False) -> None:
"""配置窗口状态"""
def __currentChanged(self, index: int) -> None:
"""切换界面时任务"""
self.switch_theme()
if mode == "显示主窗口":
# 配置主窗口
if not self.window().isVisible():
size = list(
map(
int,
Config.get(Config.ui_size).split("x"),
)
)
location = list(
map(
int,
Config.get(Config.ui_location).split("x"),
)
)
if self.window().isMaximized():
self.window().showNormal()
self.window().setGeometry(location[0], location[1], size[0], size[1])
self.window().show()
if not if_quick:
if Config.get(Config.ui_maximized):
self.titleBar.maxBtn.click()
self.show_ui("配置托盘")
if not any(
self.window().geometry().intersects(screen.availableGeometry())
for screen in QApplication.screens()
):
self.window().showNormal()
self.window().setGeometry(100, 100, 1200, 700)
self.window().raise_()
self.window().activateWindow()
elif mode == "配置托盘":
if Config.get(Config.ui_IfShowTray):
self.tray.show()
else:
self.tray.hide()
elif mode == "隐藏到托盘":
# 保存窗口相关属性
if not self.window().isMaximized():
Config.set(
Config.ui_size,
f"{self.geometry().width()}x{self.geometry().height()}",
)
Config.set(
Config.ui_location,
f"{self.geometry().x()}x{self.geometry().y()}",
)
Config.set(Config.ui_maximized, self.window().isMaximized())
Config.save()
# 隐藏主窗口
if not if_quick:
self.window().hide()
self.tray.show()
if index == 1:
self.member_manager.reload_plan_name()
elif index == 3:
self.queue_manager.reload_member_name()
elif index == 4:
self.dispatch_center.pivot.setCurrentItem("主调度台")
self.dispatch_center.update_top_bar()
def closeEvent(self, event: QCloseEvent):
"""清理残余进程"""

File diff suppressed because it is too large Load Diff

497
app/ui/plan_manager.py Normal file
View File

@@ -0,0 +1,497 @@
# 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.3
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QStackedWidget,
QHeaderView,
)
from qfluentwidgets import (
Action,
FluentIcon,
MessageBox,
HeaderCardWidget,
CommandBar,
TableWidget,
)
from typing import List, Dict, Union
import shutil
from app.core import Config, MainInfoBar, MaaPlanConfig, SoundPlayer
from .Widget import (
ComboBoxMessageBox,
LineEditSettingCard,
ComboBoxSettingCard,
SpinBoxSetting,
EditableComboBoxSetting,
ComboBoxSetting,
PivotArea,
)
class PlanManager(QWidget):
"""计划管理父界面"""
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("计划管理")
layout = QVBoxLayout(self)
self.tools = CommandBar()
self.plan_manager = self.PlanSettingBox(self)
# 逐个添加动作
self.tools.addActions(
[
Action(FluentIcon.ADD_TO, "新建计划表", triggered=self.add_setting_box),
Action(
FluentIcon.REMOVE_FROM, "删除计划表", triggered=self.del_setting_box
),
]
)
self.tools.addSeparator()
self.tools.addActions(
[
Action(
FluentIcon.LEFT_ARROW, "向左移动", triggered=self.left_setting_box
),
Action(
FluentIcon.RIGHT_ARROW, "向右移动", triggered=self.right_setting_box
),
]
)
self.tools.addSeparator()
layout.addWidget(self.tools)
layout.addWidget(self.plan_manager)
def add_setting_box(self):
"""添加一个计划表"""
choice = ComboBoxMessageBox(
self.window(),
"选择一个计划类型以添加相应计划表",
["选择计划类型"],
[["MAA"]],
)
if choice.exec() and choice.input[0].currentIndex() != -1:
if choice.input[0].currentText() == "MAA":
index = len(Config.plan_dict) + 1
maa_plan_config = MaaPlanConfig()
maa_plan_config.load(
Config.app_path / f"config/MaaPlanConfig/计划_{index}/config.json",
maa_plan_config,
)
maa_plan_config.save()
Config.plan_dict[f"计划_{index}"] = {
"Type": "Maa",
"Path": Config.app_path / f"config/MaaPlanConfig/计划_{index}",
"Config": maa_plan_config,
}
self.plan_manager.add_MaaPlanSettingBox(index)
self.plan_manager.switch_SettingBox(index)
logger.success(f"计划管理 计划_{index} 添加成功")
MainInfoBar.push_info_bar(
"success", "操作成功", f"添加计划表 计划_{index}", 3000
)
SoundPlayer.play("添加计划表")
def del_setting_box(self):
"""删除一个计划表"""
name = self.plan_manager.pivot.currentRouteKey()
if name is None:
logger.warning("删除计划表时未选择计划表")
MainInfoBar.push_info_bar(
"warning", "未选择计划表", "请选择一个计划表", 5000
)
return None
if len(Config.running_list) > 0:
logger.warning("删除计划表时调度队列未停止运行")
MainInfoBar.push_info_bar(
"warning", "调度中心正在执行任务", "请等待或手动中止任务", 5000
)
return None
choice = MessageBox("确认", f"确定要删除 {name} 吗?", self.window())
if choice.exec():
self.plan_manager.clear_SettingBox()
shutil.rmtree(Config.plan_dict[name]["Path"])
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(
Config.plan_dict[f"计划_{i}"]["Path"].with_name(f"计划_{i-1}")
)
Config.change_plan(f"计划_{i}", f"计划_{i-1}")
self.plan_manager.show_SettingBox(max(int(name[3:]) - 1, 1))
logger.success(f"计划表 {name} 删除成功")
MainInfoBar.push_info_bar("success", "操作成功", f"删除计划表 {name}", 3000)
SoundPlayer.play("删除计划表")
def left_setting_box(self):
"""向左移动计划表"""
name = self.plan_manager.pivot.currentRouteKey()
if name is None:
logger.warning("向左移动计划表时未选择计划表")
MainInfoBar.push_info_bar(
"warning", "未选择计划表", "请选择一个计划表", 5000
)
return None
index = int(name[3:])
if index == 1:
logger.warning("向左移动计划表时已到达最左端")
MainInfoBar.push_info_bar(
"warning", "已经是第一个计划表", "无法向左移动", 5000
)
return None
if len(Config.running_list) > 0:
logger.warning("向左移动计划表时调度队列未停止运行")
MainInfoBar.push_info_bar(
"warning", "调度中心正在执行任务", "请等待或手动中止任务", 5000
)
return None
self.plan_manager.clear_SettingBox()
Config.plan_dict[name]["Path"].rename(
Config.plan_dict[name]["Path"].with_name("计划_0")
)
Config.change_plan(name, "计划_0")
Config.plan_dict[f"计划_{index-1}"]["Path"].rename(
Config.plan_dict[name]["Path"]
)
Config.change_plan(f"计划_{index-1}", name)
Config.plan_dict[name]["Path"].with_name("计划_0").rename(
Config.plan_dict[f"计划_{index-1}"]["Path"]
)
Config.change_plan("计划_0", f"计划_{index-1}")
self.plan_manager.show_SettingBox(index - 1)
logger.success(f"计划表 {name} 左移成功")
MainInfoBar.push_info_bar("success", "操作成功", f"左移计划表 {name}", 3000)
def right_setting_box(self):
"""向右移动计划表"""
name = self.plan_manager.pivot.currentRouteKey()
if name is None:
logger.warning("向右移动计划表时未选择计划表")
MainInfoBar.push_info_bar(
"warning", "未选择计划表", "请选择一个计划表", 5000
)
return None
index = int(name[3:])
if index == len(Config.plan_dict):
logger.warning("向右移动计划表时已到达最右端")
MainInfoBar.push_info_bar(
"warning", "已经是最后一个计划表", "无法向右移动", 5000
)
return None
if len(Config.running_list) > 0:
logger.warning("向右移动计划表时调度队列未停止运行")
MainInfoBar.push_info_bar(
"warning", "调度中心正在执行任务", "请等待或手动中止任务", 5000
)
return None
self.plan_manager.clear_SettingBox()
Config.plan_dict[name]["Path"].rename(
Config.plan_dict[name]["Path"].with_name("计划_0")
)
Config.change_plan(name, "计划_0")
Config.plan_dict[f"计划_{index+1}"]["Path"].rename(
Config.plan_dict[name]["Path"]
)
Config.change_plan(f"计划_{index+1}", name)
Config.plan_dict[name]["Path"].with_name("计划_0").rename(
Config.plan_dict[f"计划_{index+1}"]["Path"]
)
Config.change_plan("计划_0", f"计划_{index+1}")
self.plan_manager.show_SettingBox(index + 1)
logger.success(f"计划表 {name} 右移成功")
MainInfoBar.push_info_bar("success", "操作成功", f"右移计划表 {name}", 3000)
class PlanSettingBox(QWidget):
"""计划管理子页面组"""
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("计划管理页面组")
self.pivotArea = PivotArea(self)
self.pivot = self.pivotArea.pivot
self.stackedWidget = QStackedWidget(self)
self.stackedWidget.setContentsMargins(0, 0, 0, 0)
self.stackedWidget.setStyleSheet("background: transparent; border: none;")
self.script_list: List[PlanManager.PlanSettingBox.MaaPlanSettingBox] = []
self.Layout = QVBoxLayout(self)
self.Layout.addWidget(self.pivotArea)
self.Layout.addWidget(self.stackedWidget)
self.Layout.setContentsMargins(0, 0, 0, 0)
self.pivot.currentItemChanged.connect(
lambda index: self.switch_SettingBox(
int(index[3:]), if_chang_pivot=False
)
)
self.show_SettingBox(1)
def show_SettingBox(self, index) -> None:
"""加载所有子界面"""
Config.search_plan()
for name, info in Config.plan_dict.items():
if info["Type"] == "Maa":
self.add_MaaPlanSettingBox(int(name[3:]))
self.switch_SettingBox(index)
def switch_SettingBox(self, index: int, if_chang_pivot: bool = True) -> None:
"""切换到指定的子界面"""
if len(Config.plan_dict) == 0:
return None
if index > len(Config.plan_dict):
return None
if if_chang_pivot:
self.pivot.setCurrentItem(self.script_list[index - 1].objectName())
self.stackedWidget.setCurrentWidget(self.script_list[index - 1])
def clear_SettingBox(self) -> None:
"""清空所有子界面"""
for sub_interface in self.script_list:
Config.gameid_refreshed.disconnect(sub_interface.refresh_gameid)
self.stackedWidget.removeWidget(sub_interface)
sub_interface.deleteLater()
self.script_list.clear()
self.pivot.clear()
def add_MaaPlanSettingBox(self, uid: int) -> None:
"""添加一个MAA设置界面"""
maa_plan_setting_box = self.MaaPlanSettingBox(uid, self)
self.script_list.append(maa_plan_setting_box)
self.stackedWidget.addWidget(self.script_list[-1])
self.pivot.addItem(routeKey=f"计划_{uid}", text=f"计划 {uid}")
class MaaPlanSettingBox(HeaderCardWidget):
"""MAA类计划设置界面"""
def __init__(self, uid: int, parent=None):
super().__init__(parent)
self.setObjectName(f"计划_{uid}")
self.setTitle("MAA计划表")
self.config = Config.plan_dict[f"计划_{uid}"]["Config"]
self.card_Name = LineEditSettingCard(
icon=FluentIcon.EDIT,
title="计划表名称",
content="用于标识计划表的名称",
text="请输入计划表名称",
qconfig=self.config,
configItem=self.config.Info_Name,
parent=self,
)
self.card_Mode = ComboBoxSettingCard(
icon=FluentIcon.DICTIONARY,
title="计划模式",
content="全局模式下计划内容固定,周计划模式下计划按周一到周日切换",
texts=["全局", "周计划"],
qconfig=self.config,
configItem=self.config.Info_Mode,
parent=self,
)
self.table = TableWidget(self)
self.table.setColumnCount(8)
self.table.setRowCount(6)
self.table.setHorizontalHeaderLabels(
["全局", "周一", "周二", "周三", "周四", "周五", "周六", "周日"]
)
self.table.setVerticalHeaderLabels(
[
"吃理智药",
"连战次数",
"关卡选择",
"备选 - 1",
"备选 - 2",
"剩余理智",
]
)
self.table.setAlternatingRowColors(False)
self.table.setEditTriggers(TableWidget.NoEditTriggers)
for col in range(8):
self.table.horizontalHeader().setSectionResizeMode(
col, QHeaderView.ResizeMode.Stretch
)
for row in range(6):
self.table.verticalHeader().setSectionResizeMode(
row, QHeaderView.ResizeMode.ResizeToContents
)
self.item_dict: Dict[
str,
Dict[
str,
Union[SpinBoxSetting, ComboBoxSetting, EditableComboBoxSetting],
],
] = {}
for col, (group, name_dict) in enumerate(
self.config.config_item_dict.items()
):
self.item_dict[group] = {}
for row, (name, configItem) in enumerate(name_dict.items()):
if name == "MedicineNumb":
self.item_dict[group][name] = SpinBoxSetting(
range=(0, 1024),
qconfig=self.config,
configItem=configItem,
parent=self,
)
elif name == "SeriesNumb":
self.item_dict[group][name] = ComboBoxSetting(
texts=["AUTO", "6", "5", "4", "3", "2", "1", "不选择"],
qconfig=self.config,
configItem=configItem,
parent=self,
)
elif name == "GameId_Remain":
self.item_dict[group][name] = EditableComboBoxSetting(
value=Config.gameid_dict[group]["value"],
texts=[
"不使用" if _ == "当前/上次" else _
for _ in Config.gameid_dict[group]["text"]
],
qconfig=self.config,
configItem=configItem,
parent=self,
)
elif "GameId" in name:
self.item_dict[group][name] = EditableComboBoxSetting(
value=Config.gameid_dict[group]["value"],
texts=Config.gameid_dict[group]["text"],
qconfig=self.config,
configItem=configItem,
parent=self,
)
self.table.setCellWidget(row, col, self.item_dict[group][name])
Layout = QVBoxLayout()
Layout.addWidget(self.card_Name)
Layout.addWidget(self.card_Mode)
Layout.addWidget(self.table)
self.viewLayout.addLayout(Layout)
self.viewLayout.setSpacing(3)
self.viewLayout.setContentsMargins(3, 0, 3, 3)
self.card_Mode.comboBox.currentIndexChanged.connect(self.switch_mode)
Config.gameid_refreshed.connect(self.refresh_gameid)
self.switch_mode()
def switch_mode(self) -> None:
"""切换计划模式"""
for group, name_dict in self.item_dict.items():
for name, setting_item in name_dict.items():
setting_item.setEnabled(
(group == "ALL")
== (self.config.get(self.config.Info_Mode) == "ALL")
)
def refresh_gameid(self):
for group, name_dict in self.item_dict.items():
for name, setting_item in name_dict.items():
if name == "GameId_Remain":
setting_item.reLoadOptions(
Config.gameid_dict[group]["value"],
[
"不使用" if _ == "当前/上次" else _
for _ in Config.gameid_dict[group]["text"]
],
)
elif "GameId" in name:
setting_item.reLoadOptions(
Config.gameid_dict[group]["value"],
Config.gameid_dict[group]["text"],
)

View File

@@ -34,17 +34,15 @@ from PySide6.QtWidgets import (
)
from qfluentwidgets import (
Action,
Pivot,
ScrollArea,
FluentIcon,
MessageBox,
HeaderCardWidget,
CommandBar,
)
from PySide6.QtCore import Qt
from typing import List
from app.core import QueueConfig, Config, MainInfoBar
from app.core import QueueConfig, Config, MainInfoBar, SoundPlayer
from .Widget import (
SwitchSettingCard,
ComboBoxSettingCard,
@@ -52,6 +50,7 @@ from .Widget import (
TimeEditSettingCard,
NoOptionComboBoxSettingCard,
HistoryCard,
PivotArea,
)
@@ -117,13 +116,14 @@ class QueueManager(QWidget):
logger.success(f"调度队列_{index} 添加成功")
MainInfoBar.push_info_bar("success", "操作成功", f"添加 调度队列_{index}", 3000)
SoundPlayer.play("添加调度队列")
def del_setting_box(self):
"""删除一个调度队列实例"""
name = self.queue_manager.pivot.currentRouteKey()
if name == None:
if name is None:
logger.warning("未选择调度队列")
MainInfoBar.push_info_bar(
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
@@ -137,11 +137,7 @@ class QueueManager(QWidget):
)
return None
choice = MessageBox(
"确认",
f"确定要删除 {name} 吗?",
self.window(),
)
choice = MessageBox("确认", f"确定要删除 {name} 吗?", self.window())
if choice.exec():
self.queue_manager.clear_SettingBox()
@@ -159,13 +155,14 @@ class QueueManager(QWidget):
logger.success(f"{name} 删除成功")
MainInfoBar.push_info_bar("success", "操作成功", f"删除 {name}", 3000)
SoundPlayer.play("删除调度队列")
def left_setting_box(self):
"""向左移动调度队列实例"""
name = self.queue_manager.pivot.currentRouteKey()
if name == None:
if name is None:
logger.warning("未选择调度队列")
MainInfoBar.push_info_bar(
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
@@ -210,7 +207,7 @@ class QueueManager(QWidget):
name = self.queue_manager.pivot.currentRouteKey()
if name == None:
if name is None:
logger.warning("未选择调度队列")
MainInfoBar.push_info_bar(
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
@@ -305,15 +302,19 @@ class QueueManager(QWidget):
self.setObjectName("调度队列管理")
self.pivot = Pivot(self)
self.pivotArea = PivotArea()
self.pivot = self.pivotArea.pivot
self.stackedWidget = QStackedWidget(self)
self.Layout = QVBoxLayout(self)
self.stackedWidget.setContentsMargins(0, 0, 0, 0)
self.stackedWidget.setStyleSheet("background: transparent; border: none;")
self.script_list: List[
QueueManager.QueueSettingBox.QueueMemberSettingBox
] = []
self.Layout.addWidget(self.pivot, 0, Qt.AlignHCenter)
self.Layout = QVBoxLayout(self)
self.Layout.addWidget(self.pivotArea)
self.Layout.addWidget(self.stackedWidget)
self.Layout.setContentsMargins(0, 0, 0, 0)
@@ -376,14 +377,6 @@ class QueueManager(QWidget):
self.setObjectName(f"调度队列_{uid}")
self.config = Config.queue_dict[f"调度队列_{uid}"]["Config"]
layout = QVBoxLayout()
scrollArea = ScrollArea()
scrollArea.setWidgetResizable(True)
content_widget = QWidget()
content_layout = QVBoxLayout(content_widget)
self.queue_set = self.QueueSetSettingCard(self.config, self)
self.time = self.TimeSettingCard(self.config, self)
self.task = self.TaskSettingCard(self.config, self)
@@ -393,18 +386,24 @@ class QueueManager(QWidget):
parent=self,
)
content_widget = QWidget()
content_layout = QVBoxLayout(content_widget)
content_layout.setContentsMargins(0, 0, 11, 0)
content_layout.addWidget(self.queue_set)
content_layout.addWidget(self.time)
content_layout.addWidget(self.task)
content_layout.addWidget(self.history)
content_layout.addStretch(1)
scrollArea = ScrollArea()
scrollArea.setWidgetResizable(True)
scrollArea.setContentsMargins(0, 0, 0, 0)
scrollArea.setStyleSheet("background: transparent; border: none;")
scrollArea.setWidget(content_widget)
layout = QVBoxLayout(self)
layout.addWidget(scrollArea)
self.setLayout(layout)
class QueueSetSettingCard(HeaderCardWidget):
def __init__(self, config: QueueConfig, parent=None):

View File

@@ -26,7 +26,8 @@ v4.3
"""
from loguru import logger
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout
from PySide6.QtWidgets import QWidget, QVBoxLayout
from PySide6.QtGui import QIcon
from PySide6.QtCore import Qt
from qfluentwidgets import (
ScrollArea,
@@ -48,8 +49,9 @@ from packaging import version
from pathlib import Path
from typing import Dict, Union
from app.core import Config, MainInfoBar, Network
from app.core import Config, MainInfoBar, Network, SoundPlayer
from app.services import Crypto, System, Notify
from .downloader import DownloadManager
from .Widget import (
SwitchSettingCard,
RangeSettingCard,
@@ -68,10 +70,8 @@ class Setting(QWidget):
super().__init__(parent)
self.setObjectName("设置")
content_widget = QWidget()
content_layout = QVBoxLayout(content_widget)
self.function = FunctionSettingCard(self)
self.voice = VoiceSettingCard(self)
self.start = StartSettingCard(self)
self.ui = UiSettingCard(self)
self.notification = NotifySettingCard(self)
@@ -91,7 +91,11 @@ class Setting(QWidget):
)
self.other.card_Notice.clicked.connect(lambda: self.show_notice(if_show=True))
content_widget = QWidget()
content_layout = QVBoxLayout(content_widget)
content_layout.setContentsMargins(0, 0, 11, 0)
content_layout.addWidget(self.function)
content_layout.addWidget(self.voice)
content_layout.addWidget(self.start)
content_layout.addWidget(self.ui)
content_layout.addWidget(self.notification)
@@ -101,10 +105,12 @@ class Setting(QWidget):
scrollArea = ScrollArea()
scrollArea.setWidgetResizable(True)
scrollArea.setContentsMargins(0, 0, 0, 0)
scrollArea.setStyleSheet("background: transparent; border: none;")
scrollArea.setWidget(content_widget)
layout = QVBoxLayout()
layout = QVBoxLayout(self)
layout.addWidget(scrollArea)
self.setLayout(layout)
def agree_bilibili(self) -> None:
"""授权bilibili游戏隐私政策"""
@@ -248,9 +254,7 @@ class Setting(QWidget):
choice.exec()
else:
choice = MessageBox(
"确认",
"您没有输入管理密钥,是否取消修改管理密钥?",
self.window(),
"确认", "您没有输入管理密钥,是否取消修改管理密钥?", self.window()
)
if choice.exec():
break
@@ -260,31 +264,26 @@ class Setting(QWidget):
current_version = list(map(int, Config.VERSION.split(".")))
if Network.if_running and if_show:
MainInfoBar.push_info_bar(
"warning", "请求速度过快", "上个网络请求还未结束,请稍等片刻", 5000
)
return None
# 从远程服务器获取最新版本信息
Network.set_info(
network = Network.add_task(
mode="get",
url=f"https://mirrorchyan.com/api/resources/AUTO_MAA/latest?user_agent=AutoMaaGui&current_version={version_text(current_version)}&cdk={Crypto.win_decryptor(Config.get(Config.update_MirrorChyanCDK))}&channel={Config.get(Config.update_UpdateType)}",
)
Network.start()
Network.loop.exec()
if Network.stutus_code == 200:
version_info: Dict[str, Union[int, str, Dict[str, str]]] = (
Network.response_json
)
network.loop.exec()
network_result = Network.get_result(network)
if network_result["status_code"] == 200:
version_info: Dict[str, Union[int, str, Dict[str, str]]] = network_result[
"response_json"
]
else:
if Network.response_json:
if network_result["response_json"]:
version_info = Network.response_json
version_info = network_result["response_json"]
if version_info["code"] != 0:
logger.error(f"获取版本信息时出错:{version_info["msg"]}")
logger.error(f"获取版本信息时出错:{version_info['msg']}")
error_remark_dict = {
1001: "获取版本信息的URL参数不正确",
@@ -317,11 +316,11 @@ class Setting(QWidget):
return None
logger.warning(f"获取版本信息时出错:{Network.error_message}")
logger.warning(f"获取版本信息时出错:{network_result['error_message']}")
MainInfoBar.push_info_bar(
"warning",
"获取版本信息时出错",
f"网络错误:{Network.stutus_code}",
f"网络错误:{network_result['status_code']}",
5000,
)
return None
@@ -378,45 +377,75 @@ class Setting(QWidget):
else:
all_version_info[key] = value.copy()
version_info = {
"更新总览": f"{main_version_info}\n\n{version_info_markdown(update_version_info)}",
"ALL~版本信息": version_info_markdown(all_version_info),
**{
version_text(list(map(int, k.split(".")))): version_info_markdown(v)
for k, v in version_info_json.items()
},
}
# 询问是否开始版本更新
choice = NoticeMessageBox(self.window(), "版本更新", version_info)
SoundPlayer.play("有新版本")
choice = NoticeMessageBox(
self.window(),
"版本更新",
{
"更新总览": f"{main_version_info}\n\n{version_info_markdown(update_version_info)}",
"ALL~版本信息": version_info_markdown(all_version_info),
**{
version_text(
list(map(int, k.split(".")))
): version_info_markdown(v)
for k, v in version_info_json.items()
},
},
)
if choice.exec():
with Config.version_path.open(mode="r", encoding="utf-8") as f:
version_info = json.load(f)
version_info["main_version"] = Config.VERSION
with Config.version_path.open(mode="w", encoding="utf-8") as f:
json.dump(version_info, f, ensure_ascii=False, indent=4)
if (Config.app_path / "AUTO_Updater.exe").exists():
shutil.copy(
Config.app_path / "AUTO_Updater.exe",
Config.app_path / "AUTO_Updater.active.exe",
)
if "url" in version_info["data"]:
download_config = {
"mode": "MirrorChyan",
"thread_numb": 1,
"url": version_info["data"]["url"],
}
else:
logger.error("更新器文件不存在")
MainInfoBar.push_info_bar(
"error", "更新器不存在", "请手动前往 GitHub 获取最新版本", -1
)
return None
subprocess.Popen(
[Config.app_path / "AUTO_Updater.active.exe"],
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
| subprocess.DETACHED_PROCESS
| subprocess.CREATE_NO_WINDOW,
# 从远程服务器获取代理信息
network = Network.add_task(
mode="get",
url="https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/download_info.json",
)
network.loop.exec()
network_result = Network.get_result(network)
if network_result["status_code"] == 200:
download_info = network_result["response_json"]
else:
logger.warning(
f"获取应用列表时出错:{network_result['error_message']}"
)
MainInfoBar.push_info_bar(
"warning",
"获取应用列表时出错",
f"网络错误:{network_result['status_code']}",
5000,
)
return None
download_config = {
"mode": "Proxy",
"thread_numb": Config.get(Config.update_ThreadNumb),
"proxy_list": list(
set(
Config.get(Config.update_ProxyUrlList)
+ download_info["proxy_list"]
)
),
"download_dict": download_info["download_dict"],
}
self.downloader = DownloadManager(
Config.app_path, "AUTO_MAA", remote_version, download_config
)
self.window().close()
QApplication.quit()
self.downloader.setWindowTitle("AUTO_MAA更新器")
self.downloader.setWindowIcon(
QIcon(str(Config.app_path / "resources/icons/AUTO_MAA_Updater.ico"))
)
self.downloader.download_accomplish.connect(self.start_setup)
self.downloader.show()
self.downloader.run()
elif (
if_show
@@ -433,28 +462,48 @@ class Setting(QWidget):
"发现新版本",
f"{version_text(current_version)} --> {version_text(remote_version)}",
3600000,
if_force=True,
)
SoundPlayer.play("有新版本")
else:
MainInfoBar.push_info_bar("success", "更新检查", "已是最新版本~", 3000)
SoundPlayer.play("无新版本")
def start_setup(self) -> None:
subprocess.Popen(
[
Config.app_path / "AUTO_MAA-Setup.exe",
"/SP-",
"/SILENT",
"/NOCANCEL",
"/FORCECLOSEAPPLICATIONS",
"/LANG=Chinese",
f"/DIR={Config.app_path}",
],
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
| subprocess.DETACHED_PROCESS
| subprocess.CREATE_NO_WINDOW,
)
System.set_power("KillSelf")
def show_notice(self, if_show: bool = False, if_first: bool = False) -> None:
"""显示公告"""
# 从远程服务器获取最新公告
Network.set_info(
network = Network.add_task(
mode="get",
url="https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/notice.json",
)
Network.start()
Network.loop.exec()
if Network.stutus_code == 200:
notice = Network.response_json
network.loop.exec()
network_result = Network.get_result(network)
if network_result["status_code"] == 200:
notice = network_result["response_json"]
else:
logger.warning(f"获取最新公告时出错:{Network.error_message}")
logger.warning(f"获取最新公告时出错:{network_result['error_message']}")
MainInfoBar.push_info_bar(
"warning",
"获取最新公告时出错",
f"网络错误:{Network.stutus_code}",
f"网络错误:{network_result['status_code']}",
5000,
)
return None
@@ -486,11 +535,38 @@ class Setting(QWidget):
choice = NoticeMessageBox(self.window(), "公告", notice["notice_dict"])
choice.button_cancel.hide()
choice.button_layout.insertStretch(0, 1)
SoundPlayer.play("公告展示")
if choice.exec():
with (Config.app_path / "resources/notice.json").open(
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()
@@ -499,8 +575,9 @@ class Setting(QWidget):
):
MainInfoBar.push_info_bar(
"info", "有新公告", "请前往设置界面查看公告", 3600000
"info", "有新公告", "请前往设置界面查看公告", 3600000, if_force=True
)
SoundPlayer.play("公告通知")
return None
@@ -609,6 +686,36 @@ class FunctionSettingCard(HeaderCardWidget):
self.addGroupWidget(widget)
class VoiceSettingCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("音效")
self.card_Enabled = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="音效开关",
content="是否启用音效",
qconfig=Config,
configItem=Config.voice_Enabled,
parent=self,
)
self.card_Type = ComboBoxSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="音效模式",
content="选择音效的播放模式",
texts=["简洁", "聒噪"],
qconfig=Config,
configItem=Config.voice_Type,
parent=self,
)
Layout = QVBoxLayout()
Layout.addWidget(self.card_Enabled)
Layout.addWidget(self.card_Type)
self.viewLayout.addLayout(Layout)
class StartSettingCard(HeaderCardWidget):
def __init__(self, parent=None):
@@ -1012,7 +1119,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

View File

@@ -51,7 +51,6 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
Source: "{#MyAppPath}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppPath}\app\*"; DestDir: "{app}\app"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#MyAppPath}\resources\*"; DestDir: "{app}\resources"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#MyAppPath}\AUTO_Updater.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppPath}\main.py"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppPath}\requirements.txt"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppPath}\README.md"; DestDir: "{app}"; Flags: ignoreversion
@@ -63,7 +62,7 @@ Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall
[Code]
var

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

@@ -0,0 +1,67 @@
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

View File

@@ -29,6 +29,4 @@ __version__ = "4.2.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .downloader import DownloadManager
__all__ = ["DownloadManager"]
__all__ = []

View File

@@ -66,18 +66,17 @@ if __name__ == "__main__":
version = json.load(f)
main_version_numb = list(map(int, version["main_version"].split(".")))
updater_version_numb = list(map(int, version["updater_version"].split(".")))
print("Packaging AUTO_MAA main program ...")
os.system(
"powershell -Command python -m nuitka --standalone --onefile --mingw64"
" --enable-plugins=pyside6 --windows-console-mode=disable"
" --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"
@@ -86,35 +85,10 @@ if __name__ == "__main__":
print("AUTO_MAA main program packaging completed !")
print("Packaging AUTO_MAA update program ...")
shutil.copy(root_path / "app/utils/downloader.py", root_path)
os.system(
"powershell -Command python -m nuitka --standalone --onefile --mingw64"
" --enable-plugins=pyside6 --windows-console-mode=disable"
" --onefile-tempdir-spec='{TEMP}\\AUTO_MAA_Updater'"
" --windows-icon-from-ico=resources\\icons\\AUTO_MAA_Updater.ico"
" --company-name='AUTO_MAA Team' --product-name=AUTO_MAA"
f" --file-version={version["updater_version"]}"
f" --product-version={version["main_version"]}"
" --file-description='AUTO_MAA Component'"
" --copyright='Copyright © 2024-2025 DLmaster361'"
" --assume-yes-for-downloads --output-filename=AUTO_Updater"
" --remove-output downloader.py"
)
(root_path / "downloader.py").unlink()
print("AUTO_MAA update program packaging completed !")
print("start to create setup program ...")
(root_path / "AUTO_MAA").mkdir(parents=True, exist_ok=True)
print("Start to move AUTO_MAA program ...")
shutil.move(root_path / "AUTO_MAA.exe", root_path / "AUTO_MAA/")
shutil.move(root_path / "AUTO_Updater.exe", root_path / "AUTO_MAA/")
print("Start to copy rescourses ...")
shutil.copytree(root_path / "app", root_path / "AUTO_MAA/app")
shutil.copytree(root_path / "resources", root_path / "AUTO_MAA/resources")
shutil.copy(root_path / "main.py", root_path / "AUTO_MAA/")
@@ -122,34 +96,8 @@ if __name__ == "__main__":
shutil.copy(root_path / "README.md", root_path / "AUTO_MAA/")
shutil.copy(root_path / "LICENSE", root_path / "AUTO_MAA/")
print("Start to compress ...")
shutil.make_archive(
base_name=root_path / f"AUTO_MAA_{version_text(main_version_numb)}",
format="zip",
root_dir=root_path / "AUTO_MAA",
base_dir=".",
)
shutil.rmtree(root_path / "AUTO_MAA")
print("compress completed !")
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{version_text(updater_version_numb)}\n<!--{json.dumps(version["version_info"], ensure_ascii=False)}-->\n{version_info_markdown(all_version_info)}",
encoding="utf-8",
)
with (root_path / "app/utils/AUTO_MAA.iss").open(mode="r", encoding="utf-8") as f:
iss = f.read()
iss = (
iss.replace(
'#define MyAppVersion ""',
@@ -160,6 +108,36 @@ if __name__ == "__main__":
)
.replace('#define OutputDir ""', f'#define OutputDir "{root_path}"')
)
with (root_path / "AUTO_MAA.iss").open(mode="w", encoding="utf-8") as f:
f.write(iss)
os.system(f'ISCC "{root_path / "AUTO_MAA.iss"}"')
(root_path / "AUTO_MAA_Setup").mkdir(parents=True, exist_ok=True)
shutil.move(root_path / "AUTO_MAA-Setup.exe", root_path / "AUTO_MAA_Setup")
shutil.make_archive(
base_name=root_path / f"AUTO_MAA_{version_text(main_version_numb)}",
format="zip",
root_dir=root_path / "AUTO_MAA_Setup",
base_dir=".",
)
print("setup program created !")
(root_path / "AUTO_MAA.iss").unlink(missing_ok=True)
shutil.rmtree(root_path / "AUTO_MAA")
shutil.rmtree(root_path / "AUTO_MAA_Setup")
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",
)

21
main.py
View File

@@ -25,6 +25,25 @@ v4.3
作者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
from PySide6.QtWidgets import QApplication
from qfluentwidgets import FluentTranslator
@@ -42,7 +61,7 @@ def main():
from app.ui.main_window import AUTO_MAA
window = AUTO_MAA()
window.show_ui("显示主窗口")
window.show_ui("显示主窗口", if_start=True)
window.start_up_task()
sys.exit(application.exec())

View File

@@ -3,11 +3,11 @@ plyer
PySide6
PySide6-Fluent-Widgets[full]
psutil
opencv-python
pywin32
pyautogui
keyboard
pycryptodome
requests
markdown
Jinja2
nuitka
nuitka
pillow

View File

@@ -58,4 +58,5 @@ G"GUI.UseTray": "True" #显示托盘图标
G"GUI.MinimizeToTray": "False" #最小化时隐藏至托盘
"Start.EmulatorPath" #模拟器路径
"Start.EmulatorAddCommand": "-v 2" #附加命令
"Start.EmulatorWaitSeconds": "10" #等待模拟器启动时间
G"VersionUpdate.package": "MirrorChyanAppv5.15.6.zip" #更新包标识

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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,61 +1,37 @@
{
"main_version": "4.3.6.0",
"updater_version": "1.0.0.0",
"announcement": "\n## 新增功能\n- 屏蔽MuMu模拟器开屏广告功能上线\n- 更新器支持多线程下载\n- 添加强制关闭ADB与模拟器等增强任务项\n## 修复BUG\n- 修复统计信息HTML模板公招匹配错误\n- 修复密码显示按钮动画异常\n- 修复`检测到MAA未能实际执行任务`报错被异常屏蔽\n- 修复MAA超时判定异常失效\n## 程序优化\n- 关机等电源操作添加100s倒计时\n- 人工排查弹窗方法优化\n- 人工排查时自动屏蔽静默操作\n- 公告样式优化",
"main_version": "4.3.10.0",
"version_info": {
"4.3.6.0": {
"4.3.10.0": {
"新增功能": [
"适配MAA连战次数`AUTO`模式"
]
},
"4.3.6.5": {
"新增功能": [
"release中添加安装程序"
]
},
"4.3.6.4": {
"修复BUG": [
"修复详细模式下非定时自定义基建无法正常换班的问题"
]
},
"4.3.6.3": {
"新增功能": [
"下载器仅支持调用Mirror酱"
],
"修复BUG": [
"修复Mirror酱更新检查报错不显示详细信息的问题"
]
},
"4.3.6.2": {
"新增功能": [
"新增`无人值守模式`"
],
"修复BUG": [
"修复软件窗口最大化异常问题",
"修复异常操作导致窗口离开屏幕后难以复原的问题",
"修正剩余理智关卡文案",
"修复隐藏到托盘时,托盘无法退出主程序的问题",
"修复Server酱网络异常导致的卡死问题"
"更换全新默认主页图",
"适配 MAA 无`Default`配置情况 #52"
],
"程序优化": [
"主窗口显示版本号"
"静默模式控制时段延长至模拟器完成启动的10s后"
]
},
"4.3.6.1": {
"4.3.10.3": {
"程序优化": [
"使用 keyboard 模块替代 pyautogui 模块"
]
},
"4.3.10.2": {
"新增功能": [
"单次自动代理任务中,已完成的子任务在重复执行时不再启用"
"公招喜报模板优化",
"支持使用命令行调用"
],
"修复BUG": [
"修复更新动作重复执行问题"
],
"程序优化": [
"Mirror 酱链接添加`source`字段,用于标识来源",
"优化下载器测速中止条件"
]
},
"4.3.10.1": {
"新增功能": [
"森空岛签到功能上线"
]
}
},
"proxy_list": [
"https://gitproxy.click/",
"https://cdn.moran233.xyz/",
"https://gh.llkk.cc/",
"https://github.akams.cn/",
"https://www.ghproxy.cn/",
"https://ghfast.top/"
],
"download_dict": {
"官方下载站-jp": "https://jp-download.fearr.xyz/AUTO_MAA/"
}
}