Compare commits

...

338 Commits

Author SHA1 Message Date
AoXuan
539cb1de99 Merge pull request #37 from 134820134820/fix(config)-修复统计关卡掉落错误
fix(config):修复相同战斗关卡的掉落物累加问题
2025-04-11 10:22:53 +08:00
2e9ff47dbb fix(config): 移除了剿灭模式的特殊处理逻辑、优化累加掉落数据的效率 2025-04-11 10:18:42 +08:00
MistEO
c01079af1b ci: add mirrorchyan uploading (#38)
* Create mirrorchyan.yml

* Merge pull request #1 from MistEO/patch-2

* Update build-app.yml

* Update build-pre.yml
2025-04-11 09:09:04 +08:00
Dave-Desktop
cca1acb6f6 fix(config):修复相同战斗关卡的掉落物累加问题 2025-04-11 02:12:14 +10:00
DLmaster
fe26f29f93 test(ci): 测试新下载站-2 2025-03-17 21:06:33 +08:00
DLmaster
67b8725156 test(ci): 测试新下载站-1 2025-03-17 20:52:39 +08:00
DLmaster
2a235b2bc9 test(ci): 测试新下载站 2025-03-17 18:52:11 +08:00
DLmaster
54b697f2ee Merge branch 'dev' 2025-03-07 11:56:54 +08:00
DLmaster
70df428825 fix(rescourse): 修复统计信息HTML模板公招匹配错误 2025-03-07 11:56:28 +08:00
DLmaster
8993d66056 Merge branch 'dev' 2025-03-07 01:19:18 +08:00
DLmaster
863e6fb25e Merge branch 'notice_dev' into dev 2025-03-06 23:43:10 +08:00
DLmaster
181856173e feat(core): 调整获取主题图像的时机 2025-03-06 23:42:59 +08:00
DLmaster
576fe59bbc fix(models): 修复任务被手动中止项目无法被正常录入的问题 2025-03-06 23:37:51 +08:00
DLmaster
c73aca71f7 feat(core): 优化通知服务
- 优化相关设置项排布
- 邮件添加HTML模板
2025-03-06 23:02:23 +08:00
DLmaster
ce264de963 feat(ui): 6星公招通知添加独立设置项 2025-03-05 16:00:48 +08:00
DLmaster
1feb0cf83f fix(serices): 修复通知服务使用InfoBar报错时程序崩溃问题 2025-03-05 15:11:57 +08:00
DLmaster
6292624d41 fix(services): 添加邮箱通知校验过程 2025-03-05 14:34:15 +08:00
4271a07f03 fix: 修复企业微信群机器人推送通知消息未正确换行 2025-03-05 14:07:03 +08:00
DLmaster
254fb6916f feat(core): 初步完成六星公招通知 2025-03-03 21:59:37 +08:00
DLmaster
21857325a2 Merge branch 'notice_dev' into dev 2025-03-03 20:41:17 +08:00
DLmaster
175d6860a3 feat(core): 添加统计信息通知功能 2025-03-03 20:40:28 +08:00
DLmaster
d1c8f98408 feat(ui): 主页添加主题活动信息 2025-03-02 17:14:09 +08:00
DLmaster
3499fa9067 feat(utils): 更新器拥有多网址测速功能 2025-03-02 15:32:56 +08:00
DLmaster
cca2cd774c chore(release): 发版 v4.2.4-beta.6 2025-03-01 22:53:08 +08:00
DLmaster
6d60f8adb8 Merge branch 'gui_dev' into dev 2025-03-01 22:51:30 +08:00
DLmaster
3b406a7974 feat(gui): 主页功能完善 2025-03-01 22:49:51 +08:00
a116b3359c fix: 修复掉落统计正则表达式错误统计时间的问题 2025-02-27 16:04:45 +08:00
928019390b chore(release): 发版 v4.2.4-beta.5 2025-02-26 21:58:29 +08:00
022b698f54 fix: 添加剿灭模式特殊适配 2025-02-26 16:13:46 +08:00
0228ac8393 fix: 修复 RMA70-12 匹配正则表达式 2025-02-26 16:12:42 +08:00
DLmaster
a99f381f7f fix(ui): 换个正常的主页 2025-02-22 00:22:57 +08:00
DLmaster
7c0af24bf5 fix(ui): 修正部分主页网址 2025-02-21 21:39:16 +08:00
DLmaster
d3aa45cfb9 Merge branch 'DLMS_dev' into dev 2025-02-21 18:36:32 +08:00
DLmaster
f5461deb81 feat(ui): 软件主页初步完成 2025-02-21 18:35:55 +08:00
DLmaster
c19068128f feat(core): 添加启动时直接最小化功能 2025-02-21 16:15:55 +08:00
DLmaster
1367daf1b7 feat(ui): 添加软件临时主页 2025-02-21 15:54:28 +08:00
DLmaster
5fc6e74cd6 Merge branch 'log_dev' into dev 2025-02-21 12:05:08 +08:00
DLmaster
5d7227c009 feat(ui): 初步完成历史记录前端适配 2025-02-21 12:04:52 +08:00
3a9c670172 feat(core): 新增日志管理功能
- 在配置文件中添加日志保存和保留天数设置项
- 实现日志保存功能,每次运行后保存日志到指定目录
- 添加日志分析功能,掉落信息并保存为 JSON 文件
- 在设置界面新增日志管理相关配置选项

todo: 日志清理可能有问题、多账号日志可能会保存为上一个账号的日志(加了time.sleep还没测)
2025-02-18 17:29:13 +08:00
DLmaster
2768faed53 fix(core): 修正更新器方法;取消MAA运行中自动更新;补充MAA监测字段 2025-02-18 16:17:03 +08:00
DLmaster
85f3d6f09f fix(core): 修改下载器默认下载目录为/script 2025-02-17 22:54:45 +08:00
DLmaster
c99707ecb4 Merge branch 'dev' 2025-02-17 18:09:42 +08:00
DLmaster
2b8e648fe6 Merge branch 'DLMS_dev' into dev 2025-02-17 18:08:45 +08:00
DLmaster
fcf61fd39a feat(core): 接入镜像源 2025-02-17 18:06:05 +08:00
DLmaster
8e3026f91e ci(build): 激活测试流程 2025-02-17 14:27:06 +08:00
DLmaster
8e00676faf ci(build): 测试服务器上传功能 2025-02-17 14:20:23 +08:00
DLmaster
ae293c4c20 Merge pull request #28 from ClozyA:dev
ci(build): 增加预发布版本服务器上传
2025-02-17 13:28:53 +08:00
df4a5f3318 ci(build): 增加预发布版本服务器上传 2025-02-17 12:02:03 +08:00
DLmaster
1da96c4d1d fix(ui): 矫正老板键文案 2025-02-17 11:06:07 +08:00
DLmaster
144c7f5db7 feat(modles): 配置MAA前关闭可能未正常退出的MAA进程 2025-02-16 18:44:46 +08:00
DLmaster
b3a3ccfea3 feat(core): 恢复启动后直接运行主任务功能以及相关托盘菜单 2025-02-16 14:56:25 +08:00
DLmaster
c3212f0ca1 Merge branch 'DLMS_dev' into dev 2025-02-15 17:32:47 +08:00
DLmaster
a946999782 fix: 修正版本号 2025-02-15 17:32:33 +08:00
DLmaster
284c41feb7 fix(ui): 适配深色模式 2025-02-15 17:28:54 +08:00
DLmaster
ddf905cb13 docs: 文档站试运行 2025-02-15 15:55:25 +08:00
DLmaster
d5082d18ef fix: 更正版本描述 2025-02-14 23:21:38 +08:00
DLmaster
af51831062 Merge branch 'DLMS_dev' into dev 2025-02-14 16:51:04 +08:00
DLmaster
fe4707df84 feat(ui): 优化主调度台默认选项 2025-02-14 16:50:45 +08:00
DLmaster
292e7f51e0 chore(core): 升级日志监看方法 2025-02-14 16:27:18 +08:00
DLmaster
8936b1c41d fix(core): 修复部分异常;添加高级代理文件校验过程 2025-02-13 13:03:44 +08:00
DLmaster
d45da439bd chore(models): 优化MAA关闭方法 2025-02-12 22:15:17 +08:00
DLmaster
7dc057e30f feat(models): 简洁用户列表下相邻两个任务间的切换方式 2025-02-12 22:06:04 +08:00
DLmaster
eb2f9d2cea fix(models): 修复静默代理标记移除异常情况 2025-02-11 13:47:12 +08:00
DLmaster
fb1895c4eb Merge branch 'dev' 2025-02-10 19:26:25 +08:00
DLmaster
90d3dad8c8 fix(services): 修复邮箱发信人信息错误 2025-02-10 11:29:27 +08:00
DLmaster
de12339c3e Merge branch 'DLMS_dev' into dev 2025-02-09 21:36:57 +08:00
DLmaster
f07cd2b44a feat(core): 邮箱推送功能调整,改由用户提供发信邮箱 2025-02-09 21:36:33 +08:00
DLmaster
c7fbbf6f50 Merge branch 'DLMS_dev' into dev 2025-02-08 16:58:23 +08:00
DLmaster
de262ee6bd fix(models): 修复设置MAA时异常调用B服任务设置 2025-02-08 16:58:11 +08:00
DLmaster
0c123e9389 feat(services): 通知标题添加脚本实例信息 2025-02-08 12:25:48 +08:00
DLmaster
d13fbb063d feat(core): 初步完成托管bilibili游戏隐私政策功能 2025-02-08 12:05:28 +08:00
DLmaster
5c24eb7d56 docs: 改用腾讯文档展示使用方法 2025-02-07 20:15:12 +08:00
DLmaster
6c2f19a884 Revert "新增用户字段today_stauts"
This reverts commit 4ff632ed2a.
2025-02-07 19:56:41 +08:00
heziziziscool
4ff632ed2a 新增用户字段today_stauts
新增用户字段`today_status`(位于user_db),用于记录用户的代理是否代理成功,便于后续的衍生项目与二次开发。
2025-02-07 18:50:32 +08:00
DLmaster
d445c0054f Merge branch 'DLMS_dev' into dev 2025-02-07 18:27:29 +08:00
DLmaster
748fa7a004 fix(gui): 修复窗口记忆功能失效问题 2025-02-07 18:27:01 +08:00
DLmaster
c3e710b5cf chore(gui): 调整MAA设置目录时打开当前已配置的目录位置 2025-02-07 15:53:37 +08:00
DLmaster
a93a60d125 chore(core): 优化静默判定逻辑 2025-02-07 15:37:07 +08:00
DLmaster
07f24c6168 Merge branch 'DLMS_dev' into dev 2025-02-06 23:33:16 +08:00
DLmaster
7f5478b098 feat(core): 添加调度队列完成任务后行为选项 2025-02-06 23:33:00 +08:00
DLmaster
fb7a429ff2 Merge pull request #22 from ClozyA:auto_shutdown
feat(core): 添加运行完成后自动关机功能
2025-02-06 18:33:12 +08:00
3307793a3d feat: 添加运行完成后自动关机功能 2025-02-06 15:55:55 +08:00
DLmaster
0da9f4b7ab Merge branch 'main' into dev 2025-02-04 22:48:09 +08:00
DLmaster
f45dc3a34c chore(utils): 修改代理优先级 2025-02-04 22:47:26 +08:00
DLmaster
1c17f3d878 Merge branch 'dev' 2025-02-04 22:36:34 +08:00
DLmaster
75e4d2b290 fix(utils): 修复更新器异常并覆盖版本 2025-02-04 22:34:17 +08:00
DLmaster
32fe941735 Merge branch 'dev' 2025-02-04 21:52:20 +08:00
DLmaster
27633b1017 Merge branch 'DLMS_dev' into dev 2025-02-04 18:56:27 +08:00
DLmaster
c34ca0dea9 fix(utils): 更新代理镜像 2025-02-04 18:56:09 +08:00
DLmaster
0574e9c6cb fix(gui): 调整部分选项文案 2025-02-04 18:48:58 +08:00
DLmaster
b7f09141f1 feat(core): 添加更新类别可选项 2025-02-04 18:27:19 +08:00
DLmaster
022e59e65c fix(gha): pr时不再自动发版 2025-02-04 15:56:21 +08:00
DLmaster
a0731331a8 fix(gui): 修复高级MAA配置序号错位;修复高级用户无法配置问题 2025-02-04 15:17:15 +08:00
DLmaster
4b01222648 Merge branch 'dev' into DLMS_dev 2025-02-04 14:16:07 +08:00
DLmaster
cae4b26c89 Merge branch 'dev' of https://github.com/DLmaster361/AUTO_MAA into dev 2025-02-04 14:08:27 +08:00
DLmaster
427c2332f5 chore: 补充版本信息 2025-02-04 14:08:13 +08:00
heziziziscool
6f0aec329b 新增代理成功消息推送渠道Server酱与企业微信群机器人推送 2025-02-04 14:07:45 +08:00
DLmaster
4e4d1d068f chore: 补充版本信息 2025-02-03 20:12:19 +08:00
DLmaster
074f4f2ca9 Merge branch 'hz_dev' into dev 2025-02-03 20:05:34 +08:00
heziziziscool
c51f9ad901 新增代理成功消息推送渠道Server酱与企业微信群机器人推送 2025-02-03 19:22:36 +08:00
heziziziscool
792452c048 Revert "revert 提交到main"
This reverts commit 662eb0bc7f.
2025-02-03 19:00:01 +08:00
heziziziscool
662eb0bc7f revert 提交到main 2025-02-03 18:57:40 +08:00
heziziziscool
94a9bdbb93 Revert "- 新增Server酱与企业微信群机器人推送代理成功渠道。"
This reverts commit df96183f42.
2025-02-03 18:54:09 +08:00
heziziziscool
df96183f42 - 新增Server酱与企业微信群机器人推送代理成功渠道。 2025-02-03 18:49:46 +08:00
DLmaster
a5b4f6f59f fix(gui): 修复主调度台运行时选项变动问题 2025-02-03 16:20:26 +08:00
DLmaster
6f7497cbe9 fix(utils): 修复更新器文件夹定位问题 2025-02-03 09:47:10 +08:00
DLmaster
dbdc2144b7 Merge branch 'dev' 2025-02-02 09:26:20 +08:00
DLmaster
e34106f857 Merge branch 'fix_inf_dev' into dev 2025-02-02 01:20:01 +08:00
DLmaster
c3c07804cd fix(core): 修复自定义基建无法正常使用的问题
feat(core): 添加用户每日代理次数上限功能
2025-02-02 01:19:46 +08:00
DLmaster
0b5ac6bb6e Merge branch 'dev' 2025-01-31 22:03:03 +08:00
DLmaster
ea87eefb9b doc: 修复部分图片位置 2025-01-31 22:02:51 +08:00
DLmaster
20dc4656dc Merge branch 'dev' 2025-01-31 21:52:57 +08:00
DLmaster
3f0f1612e3 doc: README同步至v4.2.2版本 2025-01-31 21:52:31 +08:00
DLmaster
e92b6ecfe6 fix(maa): 修正人工排查文案 2025-01-29 10:24:03 +08:00
DLmaster
37502b6fd8 Merge branch 'dev' 2025-01-28 20:05:58 +08:00
DLmaster
8c1c3c5675 发布v4.2.2 2025-01-28 20:04:56 +08:00
DLmaster
b4228e3f35 fix(core): 补充公告逻辑 2025-01-28 19:28:04 +08:00
DLmaster
e78f3973be fix(services): 修复管理密钥修改逻辑 2025-01-28 17:28:51 +08:00
DLmaster
29536003a4 fix(core): 修复调度队列为空时定时执行被反复调起 2025-01-28 16:13:12 +08:00
DLmaster
ffa3767198 fix(package): 固定nuitka版本号 2025-01-28 14:53:36 +08:00
DLmaster
8702070725 test package -2 2025-01-28 13:36:01 +08:00
DLmaster
793259a194 test package -1 2025-01-28 12:50:49 +08:00
DLmaster
3a63a73244 fix(test): 补充更新公告 2025-01-28 11:55:50 +08:00
DLmaster
43448cc68d fix(timer): 修复反复记录FailSafeException影响log阅读 2025-01-28 10:22:17 +08:00
DLmaster
f0d272cce5 Merge branch 'user_edit_dev' into dev 2025-01-27 23:18:34 +08:00
DLmaster
9983455b60 fix(maa): 修复手机号码不全时发生的混乱 2025-01-27 23:10:46 +08:00
DLmaster
0a411c150a fix(core): 规范自动化构筑流程 2025-01-27 22:29:45 +08:00
DLmaster
89b49a1143 Merge branch 'audio_dev' into dev 2025-01-27 22:17:54 +08:00
DLmaster
917454c0b9 fix(core): 调整公告方式 2025-01-27 22:17:31 +08:00
DLmaster
170b87e7a8 feat(core): 添加公告功能 2025-01-27 21:40:34 +08:00
DLmaster
68db248a7e 重复打包 2025-01-27 17:06:35 +08:00
DLmaster
24614099ed Merge branch 'fluent-gui-dev' into dev 2025-01-27 12:32:12 +08:00
DLmaster
8bbfdcbc04 fix(utils): 修复 version_text 引起的打包问题 2025-01-27 12:20:07 +08:00
DLmaster
126799d2a2 feat(core): 恢复基本所有原功能,数据文件版本更新 2025-01-27 10:38:17 +08:00
DLmaster
c625354dec feat(core):初步完成主调度自动代理功能开发 2025-01-26 07:58:33 +08:00
DLmaster
7e08c88a3e feat(core):初步完成定时执行功能开发 2025-01-25 18:00:56 +08:00
DLmaster
764d0afb50 Release v4.2.1 2025-01-22 19:01:01 +08:00
DLmaster
fe87547406 Merge pull request #16 from DLmaster361/dev
对于MAAv5.12.1版本后两个字段`Start.RunDirectly`与`Start.OpenEmulatorAfterLaunch`的适配
2025-01-22 18:53:19 +08:00
heziziziscool
a1fd27722b Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	app/models/MAA.py
2025-01-22 18:29:07 +08:00
heziziziscool
3bd6611cd5 对于MAAv5.12.1版本后两个字段Start.RunDirectlyStart.OpenEmulatorAfterLaunch的适配 2025-01-22 18:28:42 +08:00
heziziziscool
f3e1b4580a 对于MAAv5.12.1版本后两个字段Start.RunDirectlyStart.OpenEmulatorAfterLaunch的适配 2025-01-22 17:24:52 +08:00
heziziziscool
449d8a032e 对于MAAv5.12.1版本后两个字段Start.RunDirectlyStart.OpenEmulatorAfterLaunch的适配 2025-01-22 17:24:26 +08:00
heziziziscool
b40fa35622 对于MAAv5.12.1版本后两个字段Start.RunDirectlyStart.OpenEmulatorAfterLaunch的适配 2025-01-22 17:22:01 +08:00
DLmaster
ff7e433634 fix(gui): 修复界面显示的若干方法 2025-01-12 11:59:22 +08:00
DLmaster
1ea9d10bb4 修复开机自启时目录错误 2025-01-07 11:32:06 +08:00
DLmaster
04fd2b10fa 修改缓存文件夹 2025-01-06 18:45:48 +08:00
DLmaster
e79417ec5e 调度队列逻辑修复 2025-01-06 16:23:45 +08:00
DLmaster
684211c129 初步完成调度队列开发 2025-01-05 19:24:01 +08:00
DLmaster
f94e129cba 添加启动界面 2025-01-04 14:22:45 +08:00
DLmaster
87a140d373 覆盖pre版本 2025-01-04 04:03:27 +08:00
DLmaster
f135a6510f 设置界面与实例管理界面初步重构 2025-01-04 04:02:20 +08:00
DLmaster
6960184911 回退版本 2025-01-02 10:37:49 +08:00
DLmaster
7bd270c662 Merge branch 'fluent-gui-dev' 2025-01-01 21:55:16 +08:00
DLmaster
f54e83673f 初步完成UI美化 2025-01-01 21:50:40 +08:00
DLmaster
ee40fdb3c3 Merge branch 'main' 2025-01-01 11:54:55 +08:00
DLmaster
52ebf7b027 改用pathlib处理文件目录 2025-01-01 11:52:38 +08:00
DLmaster
85891dc918 修正更新器产品号 2024-12-30 23:16:21 +08:00
DLmaster
7348a87a20 优化自动化打包流程显示 2024-12-30 22:11:50 +08:00
DLmaster
1dfa3e3f44 Merge branch 'Dev' 2024-12-30 20:16:22 +08:00
DLmaster
37ced2e535 修改自动化流程提示文本样式 2024-12-30 20:01:38 +08:00
DLmaster
11876acc62 更新版本至4.2.0,添加nuitka支持,优化项目结构并模块化功能组件 2024-12-30 19:51:57 +08:00
DLmaster
756c0926ec 完善打包流程 2024-12-30 19:41:25 +08:00
DLmaster
9604fc9a8e 修正打包参数 2024-12-29 17:35:36 +08:00
DLmaster
5bcc527889 优化打包参数 2024-12-29 17:18:28 +08:00
DLmaster
0d616289ed 添加pyautogui异常逻辑捕获处理 2024-12-29 17:10:27 +08:00
DLmaster
a8473b6a04 清除无效整合步骤 2024-12-29 01:55:48 +08:00
DLmaster
e1352586b7 Updater改为独立打包 2024-12-29 01:42:03 +08:00
DLmaster
849d5f18eb 添加新架构测试文件 2024-12-29 00:01:55 +08:00
DLmaster
77298f4dab 修正工作流名称 2024-12-28 23:59:06 +08:00
DLmaster
b7a2b045fb 创建全新模块化架构 2024-12-28 23:31:50 +08:00
DLmaster
c7072da81d 同步“启动MAA后直接最小化”字段 2024-12-27 20:48:54 +08:00
DLmaster
4c9b9fb74a tips文案更新 2024-12-25 23:03:29 +08:00
DLmaster
feb4b516a4 修复初始设置的异常 2024-12-24 22:28:40 +08:00
DLmaster
d50191c6b8 添加psutil库 2024-12-23 21:19:24 +08:00
DLmaster
3db3fa4e1f 后台静默代理功能上线 2024-12-23 20:30:12 +08:00
DLmaster
ad31961c2b 修正使用方法适配最新版本 2024-12-19 17:52:16 +08:00
DLmaster
ccdaefeb61 统一标点符号 2024-12-18 02:51:54 +08:00
DLmaster
8c9bcba198 修正更新器的若干问题 2024-12-18 02:41:40 +08:00
DLmaster
a273af0abe 更新器优化;添加邮件仅推送异常信息选项 2024-12-18 02:04:40 +08:00
DLmaster
05a063b001 修改配置方法 2024-12-14 13:41:15 +08:00
DLmaster
8487195512 调整通知停留时长 2024-12-10 20:48:09 +08:00
DLmaster
250c47ccb7 添加托盘中止任务选项 2024-12-10 19:59:12 +08:00
DLmaster
e5aeb4f3a7 添加托盘名称 2024-12-05 22:12:57 +08:00
DLmaster
eab8d984e6 Merge branch 'main' of https://github.com/DLmaster361/AUTO_MAA 2024-12-05 21:37:14 +08:00
DLmaster
6b3f19a618 修复浅色模式下UI异常 2024-12-05 21:35:32 +08:00
DLmaster
d5f8871064 重构MainTimer逻辑 2024-12-05 17:21:39 +08:00
DLmaster
77e899957c Merge pull request #11 from ClozyA/patch-1
fix: MAA项目地址
2024-12-04 15:54:23 +08:00
AoXuan
dd3515305c fix: MAA项目地址 2024-12-04 15:17:49 +08:00
DLmaster
73c3ec4820 修复深色模式下UI异常 2024-12-04 14:27:58 +08:00
DLmaster
fde5160d56 补充重要声明 2024-12-01 10:46:10 +08:00
DLmaster
13923705e5 修复窗口管理方面的部分问题 2024-12-01 00:16:49 +08:00
DLmaster
d3afc95261 启动时不显示托盘 2024-11-29 18:58:50 +08:00
DLmaster
326d7e474c 修复新用户无法设置MAA路径 2024-11-29 18:02:16 +08:00
DLmaster
537b5d9725 移除最小化启动模拟器选项 2024-11-29 11:01:55 +08:00
DLmaster
697c1b5b43 优化记忆窗口与最小化到托盘的体验 2024-11-28 20:43:12 +08:00
DLmaster
2b6057161e 更新时彻底关闭主程序 2024-11-27 21:17:05 +08:00
DLmaster
8e2a6444bd 修复设定时刻无法中止任务的问题 2024-11-27 20:36:51 +08:00
DLmaster
b04df40b7d 图标优化;添加最小化到托盘功能;更新器摆脱UI文件限制 2024-11-27 00:04:37 +08:00
DLmaster
3e17d94904 合并MAA启动器配置流程 2024-11-21 19:52:15 +08:00
DLmaster
29123054cc 3^3 2024-11-19 20:31:52 +08:00
DLmaster
530c038985 3^3 2024-11-19 20:25:53 +08:00
DLmaster
2e9d2f0491 添加软件介绍 2024-11-19 20:20:46 +08:00
DLmaster
69ab9b4f15 邮件通知功能上线 2024-11-16 10:42:56 +08:00
DLmaster
1c8b940925 记忆窗口位置 2024-11-15 10:08:50 +08:00
DLmaster
0e05a4ea18 修正version配置 2024-11-14 15:50:06 +08:00
DLmaster
9e4285b4c5 发布4.1.2.0 2024-11-14 15:40:46 +08:00
DLmaster
e89d2af8ae 发布4.1.2正式版 2024-11-14 15:35:32 +08:00
DLmaster
8e7d663060 补充注释 2024-11-10 01:51:24 +08:00
DLmaster
179b33387e 完成主程序更新后打开AUTO_MAA 2024-11-10 01:46:38 +08:00
DLmaster
2c9a7c443f 加注释 2024-11-10 01:30:51 +08:00
DLmaster
0de964e68c 修复解压失败时本地版本号异常变动问题 2024-11-09 16:30:25 +08:00
DLmaster
97046af931 version修复缺失字段 2024-11-09 16:24:44 +08:00
DLmaster
ec24e776e7 修复#9与人工排查模式报错 2024-11-09 16:09:28 +08:00
DLmaster
92d4ef05fa 进一步限制log_text长度 2024-11-09 11:58:16 +08:00
DLmaster
fb27c50c75 解决log_text过长导致的log显示于中间 2024-11-09 11:15:44 +08:00
DLmaster
0141bf039a 添加清理可能存在的临时文件步骤 2024-11-08 22:11:48 +08:00
DLmaster
54f9bb7f21 修复url错误 2024-11-08 22:05:06 +08:00
DLmaster
2c4e5eb5cc 发布v4.1.2 2024-11-08 21:57:28 +08:00
DLmaster
cd42f45a1f 打包目录结构优化 2024-11-08 20:32:06 +08:00
DLmaster
ec38895765 修改更新器图标 2024-11-08 19:01:16 +08:00
DLmaster
fc30579bbc 修复部分组件缩放问题 2024-11-07 23:45:19 +08:00
DLmaster
3e6223e4e5 发布公测消息 2024-11-07 23:35:04 +08:00
DLmaster
27c71124ff 尝试解决log框卡中间问题 2024-11-07 23:18:34 +08:00
DLmaster
3a8a8a36f5 去除误注释 2024-11-07 21:55:04 +08:00
DLmaster
ed6de5e806 打包方式优化 2024-11-07 21:51:15 +08:00
DLmaster
bf8a5befa3 添加无限代理天数模式 2024-11-07 21:00:32 +08:00
DLmaster
a3fe641f6b 添加启动AUTO_MAA后直接代理功能 2024-11-07 17:25:58 +08:00
DLmaster
f4f99c25db 适配“启动MAA后自动开启模拟器”字段更改 2024-11-06 21:51:56 +08:00
DLmaster
640d80e334 当前为最新版本通知修复,报错格式化 2024-11-03 01:23:32 +08:00
DLmaster
4f196b516f 部分功能恢复 2024-11-03 00:55:18 +08:00
DLmaster
87f07bf95c 调整参数适配主分支 2024-11-02 23:57:23 +08:00
DLmaster
1bfd7891b5 Merge branch 'Updater' 2024-11-02 23:30:16 +08:00
DLmaster
472eb0ee52 修正部分措辞 2024-10-31 20:12:30 +08:00
DLmaster
7638c67da6 更详细的使用说明书 2024-10-31 20:04:17 +08:00
DLmaster
7a0c143b5b 适配MAA的adb连接文案修改 2024-10-29 08:55:01 +08:00
DLmaster
fff8e11524 恢复代理时的自动更新 2024-10-29 08:11:06 +08:00
DLmaster
af24208cf3 无命令行中止MAA进程 2024-10-26 15:31:49 +08:00
DLmaster
85bccea2dd 删除冗余代码 2024-10-26 11:20:12 +08:00
DLmaster
f7c8cac6ec 人工排查接管更多MAA启动设置 2024-10-26 10:41:02 +08:00
DLmaster
97a5ac5bb0 修复MAA切换配置选项 2024-10-25 18:50:34 +08:00
DLmaster
1d57076010 修复自定义基建默认配置 2024-10-25 18:30:15 +08:00
DLmaster
d298ac872c 优化自定义基建配置方法 2024-10-25 15:04:05 +08:00
DLmaster
c0581e781c 工作流适配新目录结构 2024-10-24 20:08:22 +08:00
DLmaster
6befd6341a gameid.txt改由主程序进行初始化 2024-10-24 20:03:23 +08:00
DLmaster
504ef8dd68 修改部分格式 2024-10-23 21:56:41 +08:00
DLmaster
127500d890 禁用MAA路径直接编辑;更新README至最新版 2024-10-23 21:41:35 +08:00
DLmaster
ab94173df7 支持B服,beta模式上线 2024-10-23 15:19:49 +08:00
DLmaster
e50697aae1 添加系统通知功能 2024-10-09 14:16:37 +08:00
DLmaster
c71389fc0b 优化MAA设置逻辑 2024-10-08 19:26:42 +08:00
DLmaster
732129ba34 修正变量命名逻辑 2024-10-07 20:08:26 +08:00
DLmaster
77dfc895ed 修复内部任务失败逻辑错误 2024-10-06 22:09:33 +08:00
DLmaster
0c6f3123f8 人工排查功能上线;修复未完成用户无法正确加载的问题 2024-10-06 15:48:06 +08:00
DLmaster
e872d30b3a 除去冗余通配符 2024-10-02 15:19:01 +08:00
DLmaster
48fa4413c8 修复打包目录结构 2024-10-02 14:54:42 +08:00
DLmaster
94e693db77 添加对内部任务失败的识别 2024-09-20 23:27:16 +08:00
DLmaster
025d328d79 修复索引错误 2024-09-10 11:55:54 +08:00
DLmaster
d086a5a152 修改更新release的逻辑 2024-09-10 08:44:10 +08:00
DLmaster
5f2337d949 修复部分语法错误 2024-09-10 08:12:38 +08:00
DLmaster
5e163181a8 去除测试标记 2024-09-10 07:50:13 +08:00
DLmaster
0913cbb813 修改部分语法错误 2024-09-10 07:40:42 +08:00
DLmaster
6aed64ce4e 添加创建zip动作 2024-09-10 07:32:32 +08:00
DLmaster
3c95e58fb2 五次测试 2024-09-09 23:02:25 +08:00
DLmaster
64d0d4ed41 四次测试 2024-09-09 22:45:05 +08:00
DLmaster
7fb659c511 三处测试 2024-09-09 22:40:00 +08:00
DLmaster
d4ec91940f 二次测试 2024-09-09 22:35:47 +08:00
DLmaster
77f63ebb44 测试 2024-09-09 22:24:34 +08:00
DLmaster
051b062e29 修复发布新版本时无法上传软件包 2024-09-09 22:14:21 +08:00
DLmaster
ec9083b556 修复发布新版本的换行符问题 2024-09-09 22:03:30 +08:00
DLmaster
156080a3b7 修复无法发布新版本 2024-09-09 21:55:08 +08:00
DLmaster
5b6effd43e 优化log文件读取策略,MAA运行判定修改前期准备 2024-09-09 21:34:06 +08:00
DLmaster
97e8d41c39 修复部分语法错误 2024-09-06 22:42:50 +08:00
DLmaster
d64ef447d2 修复部分语法错误 2024-09-06 22:35:07 +08:00
DLmaster
461b2a62b0 修复版本号不显示 2024-09-06 22:04:03 +08:00
DLmaster
f8988651b5 修复换行符 2024-09-06 21:34:06 +08:00
DLmaster
ffe865e29e 忽略部分pr以减少自动构建压力 2024-09-06 21:26:04 +08:00
DLmaster
cd5250c09f 适配最新语法规则 2024-09-06 21:20:04 +08:00
DLmaster
8f01bf6027 修复部分语法错误 2024-09-06 21:12:13 +08:00
DLmaster
81eb11f5c5 修复部分语法错误 2024-09-06 21:04:58 +08:00
DLmaster
80cadfa52c 添加覆盖更新能力 2024-09-06 20:30:57 +08:00
DLmaster
9974d70d99 修复部分语法错误 2024-09-06 20:19:28 +08:00
DLmaster
6e3e4662f4 修复部分语法错误 2024-09-06 20:12:19 +08:00
DLmaster
e2fb2e5565 修复部分语法错误 2024-09-06 19:48:30 +08:00
DLmaster
6f8be226b0 修复部分语法错误 2024-09-06 19:42:23 +08:00
DLmaster
2606f1e587 修复部分语法错误 2024-09-06 19:37:17 +08:00
DLmaster
4d1fd7ea76 修复部分语法错误 2024-09-06 19:31:58 +08:00
DLmaster
e61fbe6c69 修复部分语法错误 2024-09-06 19:18:50 +08:00
DLmaster
0c287577ca 精简步骤 2024-09-06 19:06:38 +08:00
DLmaster
a4aa562db9 修复部分语法错误 2024-09-06 17:32:26 +08:00
DLmaster
33c5ff3a52 修复部分语法错误 2024-09-06 17:03:18 +08:00
DLmaster
7c8e43bd35 修复部分语法错误 2024-09-06 16:34:05 +08:00
DLmaster
bc0014dbe6 修复部分语法错误 2024-09-06 16:26:36 +08:00
DLmaster
e41eca33bc 修复部分语法错误 2024-09-06 16:01:46 +08:00
DLmaster
e39b965459 修复部分语法错误 2024-09-06 14:57:25 +08:00
DLmaster
f2aa3d7347 修复部分语法错误 2024-09-06 14:51:33 +08:00
DLmaster
76c126284f 自动构建 2024-09-06 14:32:30 +08:00
DLmaster
44ee917d88 同步到MAA新版本的配置方法 2024-08-03 13:11:54 +08:00
DLmaster
669019c051 优化超时判定 2024-07-28 10:42:55 +08:00
DLmaster
4685c12570 Update python-app.yml
自动构建
2024-07-27 22:25:28 +08:00
DLmaster
d815529510 Update python-app.yml
自动构建
2024-07-27 22:03:02 +08:00
DLmaster
1da3620feb Update python-app.yml
自动构建
2024-07-27 22:00:23 +08:00
DLmaster
44d529c60f Update python-app.yml
自动构建
2024-07-27 21:53:49 +08:00
DLmaster
cdbcebd945 Update python-app.yml
自动构建
2024-07-27 21:51:37 +08:00
DLmaster
5bc2bf9397 Update python-app.yml
自动构建
2024-07-27 21:48:27 +08:00
DLmaster
d41419a579 Update python-app.yml
自动构建
2024-07-27 21:40:34 +08:00
DLmaster
a7e15e509e Create python-app.yml
自动构建
2024-07-27 21:30:41 +08:00
DLmaster
6518a378ac 修正v3.0_Beta版更新说明 2024-07-24 21:13:50 +08:00
DLmaster
ebb73182f7 添加开机自启与阻止休眠功能;优化配置文件处理方式 2024-07-24 20:57:15 +08:00
DLmaster
8fcc69165f 更新自述文件 2024-07-17 16:31:04 +08:00
DLmaster
5b98c58926 添加对自定义基建的支持 2024-07-17 16:19:38 +08:00
DLmaster
ce1534657b 修复禁用剿灭时的任务完成判定 2024-07-10 09:28:11 +08:00
DLmaster
e2f02ea616 修复定时判定;MAA路径运行检查 2024-07-09 11:48:58 +08:00
DLmaster
76134c2e2b 更新GUI相关自述文件 2024-07-07 14:26:47 +08:00
DLmaster
2010855139 重构,GUI初步开发 2024-07-07 12:43:40 +08:00
DLmaster
e550b9a155 添加QQ交流群 2024-06-09 17:03:20 +08:00
DLmaster
53049d4acd 添加对1-7连战适配 2024-05-26 12:31:24 +08:00
DLmaster
bb4b45bc36 请求祝福 2024-05-25 15:13:11 +08:00
DLmaster
caf37e6c99 优化剿灭逻辑 2024-04-30 22:59:01 +08:00
DLmaster
5af41d3b72 添加依赖环境描述文件 2024-04-30 21:55:08 +08:00
DLmaster
ebd3273251 修复死循环,日志读取优化,运行流程优化 2024-04-03 20:06:48 +08:00
DLmaster
63ed257a5f 添加key文件夹占位文件 2024-03-11 12:52:13 +08:00
DLmaster
3bcfe6b1d0 修复BUG:run无法异常退出,主程序检测目录错误 2024-03-11 10:27:47 +08:00
DLmaster
7b789c71ca 小修改 2024-03-09 23:19:59 +08:00
DLmaster
f78158e102 解决检查用户存在时要求管理密钥的问题 2024-03-09 15:44:12 +08:00
DLmaster
6d1c3490ba 增加发行信息重新打包 2024-03-09 14:56:12 +08:00
DLmaster
1a9af53e4d UI优化,判定优化,修复gameid错误,代理策略优化,组件独立性优化 2024-03-09 12:00:14 +08:00
DLmaster
eac2d13fee 添加开源协议 2024-02-18 00:07:34 +08:00
DLmaster
1aad39fe7f Create LICENSE 2024-02-17 22:22:21 +08:00
DLmaster
8452ba95db 为协议更改做准备 2024-02-17 21:18:14 +08:00
DLmaster
286ee9e51e 添加修改管理密钥功能 2024-02-15 20:15:05 +08:00
DLmaster
9a2a88384b Create LICENSE 2024-02-15 15:08:30 +08:00
DLmaster
68068b29e1 适配v2.1的内容更新 2024-02-11 20:38:56 +08:00
DLmaster
3b0b50670d 用户密码加密部分上线,SQL指令更新防止注入攻击 2024-02-11 19:45:28 +08:00
DLmaster
863b6dd220 重新编入Repobeats 2024-02-07 19:26:53 +08:00
DLmaster
e3c9e3088b 无足轻重的补充 2024-02-07 16:43:42 +08:00
DLmaster
27bdfc8581 更新有关图标的说明 2024-02-07 16:29:18 +08:00
DLmaster
a2b1731fac 添加README 2024-02-07 16:19:04 +08:00
DLmaster
f96c9253fe README相关图片 2024-02-07 16:06:03 +08:00
DLmaster
9e93b6ef35 2.0版本(去除对第三方代码的依赖,添加定时功能,主程序上线) 2024-02-06 21:12:49 +08:00
52 changed files with 11385 additions and 495 deletions

185
.github/workflows/build-app.yml vendored Normal file
View File

@@ -0,0 +1,185 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
name: Build AUTO_MAA
on:
push:
branches: [ "main" ]
paths-ignore:
- '**.md'
- 'LICENSE'
permissions:
contents: read
jobs:
pre_check:
name: Pre Checks
runs-on: ubuntu-latest
steps:
- name: Repo Check
id: repo_check
run: |
if [[ "$GITHUB_REPOSITORY" != "DLmaster361/AUTO_MAA" ]]; then
echo "When forking this repository to make your own builds, you have to adjust this check."
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
uses: actions/setup-python@v5
with:
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
run: |
copy app\utils\package.py .\
python package.py
- name: Read version
id: read_version
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
- name: Create Zip
id: create_zip
run: |
Compress-Archive -Path app,resources,main.py,AUTO_MAA.exe,requirements.txt,README.md,LICENSE -DestinationPath AUTO_MAA_${{ env.AUTO_MAA_version }}.zip
Compress-Archive -Path Updater.exe -DestinationPath Updater_${{ env.updater_version }}.zip
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: AUTO_MAA_${{ env.AUTO_MAA_version }}
path: |
AUTO_MAA_${{ env.AUTO_MAA_version }}.zip
Updater_${{ env.updater_version }}.zip
- name: Upload Version_Info Artifact
uses: actions/upload-artifact@v4
with:
name: version_info
path: version_info.txt
publish_release:
name: Publish release
needs: build_AUTO_MAA
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
pattern: AUTO_MAA_*
merge-multiple: true
path: artifacts
- name: Download Version_Info
uses: actions/download-artifact@v4
with:
name: version_info
path: ./
- name: Check if release exists
id: check_if_release_exists
run: |
release_id=$(gh release view $(sed 's/\r$//g' <(head -n 1 version_info.txt)) --json id --jq .id || true)
if [[ -z $release_id ]]; then
echo "release_exists=false" >> $GITHUB_OUTPUT
else
echo "release_exists=true" >> $GITHUB_OUTPUT
fi
env:
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
- name: Create release
id: create_release
if: steps.check_if_release_exists.outputs.release_exists == 'false'
run: |
set -xe
shopt -s nullglob
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"
gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" artifacts/*
env:
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
- name: Update release
id: update_release
if: steps.check_if_release_exists.outputs.release_exists == 'true'
run: |
set -xe
shopt -s nullglob
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"
gh release delete "$TAGNAME" --yes
gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" artifacts/*
env:
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
- name: Trigger MirrorChyanUploading
run: |
gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan
gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan_release_note
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Setup SSH Key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
- name: Upload Release to Server
run: |
scp -r artifacts/* ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}:/home/user/files/AUTO_MAA/
- name: Install obsutil
run: |
wget https://obs-community.obs.cn-north-1.myhuaweicloud.com/obsutil/current/obsutil_linux_amd64.tar.gz
tar -xzvf obsutil_linux_amd64.tar.gz --strip-components=1
chmod 755 obsutil
./obsutil version
- name: Upload Release to Huawei OBS
env:
OBS_AK: ${{ secrets.OBS_AK }}
OBS_SK: ${{ secrets.OBS_SK }}
OBS_ENDPOINT: ${{ secrets.OBS_ENDPOINT }}
OBS_BUCKET: ${{ secrets.OBS_BUCKET }}
run: |
./obsutil config -i $OBS_AK -k $OBS_SK -e $OBS_ENDPOINT
./obsutil cp artifacts/ obs://$OBS_BUCKET/releases/ -r -f

185
.github/workflows/build-pre.yml vendored Normal file
View File

@@ -0,0 +1,185 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
name: Build AUTO_MAA_Pre
on:
push:
branches: [ "dev" ]
paths-ignore:
- '**.md'
- 'LICENSE'
permissions:
contents: read
jobs:
pre_check:
name: Pre Checks
runs-on: ubuntu-latest
steps:
- name: Repo Check
id: repo_check
run: |
if [[ "$GITHUB_REPOSITORY" != "DLmaster361/AUTO_MAA" ]]; then
echo "When forking this repository to make your own builds, you have to adjust this check."
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
uses: actions/setup-python@v5
with:
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
run: |
copy app\utils\package.py .\
python package.py
- name: Read version
id: read_version
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
- name: Create Zip
id: create_zip
run: |
Compress-Archive -Path app,resources,main.py,AUTO_MAA.exe,requirements.txt,README.md,LICENSE -DestinationPath AUTO_MAA_${{ env.AUTO_MAA_version }}.zip
Compress-Archive -Path Updater.exe -DestinationPath Updater_${{ env.updater_version }}.zip
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: AUTO_MAA_${{ env.AUTO_MAA_version }}
path: |
AUTO_MAA_${{ env.AUTO_MAA_version }}.zip
Updater_${{ env.updater_version }}.zip
- name: Upload Version_Info Artifact
uses: actions/upload-artifact@v4
with:
name: version_info
path: version_info.txt
publish_prerelease:
name: Publish prerelease
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: Check if release exists
id: check_if_release_exists
run: |
release_id=$(gh release view $(sed 's/\r$//g' <(head -n 1 version_info.txt)) --json id --jq .id || true)
if [[ -z $release_id ]]; then
echo "release_exists=false" >> $GITHUB_OUTPUT
else
echo "release_exists=true" >> $GITHUB_OUTPUT
fi
env:
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
- name: Create prerelease
id: create_prerelease
if: steps.check_if_release_exists.outputs.release_exists == 'false'
run: |
set -xe
shopt -s nullglob
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"
gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" --prerelease artifacts/*
env:
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
- name: Update prerelease
id: update_prerelease
if: steps.check_if_release_exists.outputs.release_exists == 'true'
run: |
set -xe
shopt -s nullglob
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"
gh release delete "$TAGNAME" --yes
gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" --prerelease artifacts/*
env:
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
- name: Trigger MirrorChyanUploading
run: |
gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan
gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan_release_note
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Setup SSH Key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
- name: Upload Release to Server
run: |
scp -r artifacts/* ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}:/home/user/files/AUTO_MAA/
- name: Install obsutil
run: |
wget https://obs-community.obs.cn-north-1.myhuaweicloud.com/obsutil/current/obsutil_linux_amd64.tar.gz
tar -xzvf obsutil_linux_amd64.tar.gz --strip-components=1
chmod 755 obsutil
./obsutil version
- name: Upload Release to Huawei OBS
env:
OBS_AK: ${{ secrets.OBS_AK }}
OBS_SK: ${{ secrets.OBS_SK }}
OBS_ENDPOINT: ${{ secrets.OBS_ENDPOINT }}
OBS_BUCKET: ${{ secrets.OBS_BUCKET }}
run: |
./obsutil config -i $OBS_AK -k $OBS_SK -e $OBS_ENDPOINT
./obsutil cp artifacts/ obs://$OBS_BUCKET/releases/ -r -f

21
.github/workflows/mirrorchyan.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: mirrorchyan
on:
workflow_dispatch:
jobs:
mirrorchyan:
runs-on: macos-latest
steps:
- id: uploading
uses: MirrorChyan/uploading-action@v1
with:
filetype: latest-release
filename: "AUTO_MAA*.zip"
mirrorchyan_rid: AUTO_MAA
owner: DLmaster361
repo: AUTO_MAA
github_token: ${{ secrets.GITHUB_TOKEN }}
upload_token: ${{ secrets.MirrorChyanUploadToken }}

View File

@@ -0,0 +1,19 @@
name: mirrorchyan_release_note
on:
workflow_dispatch:
release:
types: [edited]
jobs:
mirrorchyan:
runs-on: macos-latest
steps:
- id: uploading
uses: MirrorChyan/release-note-action@v1
with:
mirrorchyan_rid: AUTO_MAA
upload_token: ${{ secrets.MirrorChyanUploadToken }}
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,133 +0,0 @@
#include"stdafx.h"
#include<iostream>
#include<Windows.h>
#include "boost/filesystem.hpp"
#include "boost/iostreams/stream.hpp"
#include "boost/format.hpp"
#include "boost/algorithm/algorithm.hpp"
#include "boost/algorithm/string.hpp"
using namespace std;
#define READ_ONE_NUM 1024
/*
ɱ<><C9B1>ָ<EFBFBD><D6B8>·<EFBFBD><C2B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>н<EFBFBD><D0BD><EFBFBD>
*/
BOOL KillSpecifiedProcess(const std::string& p_strPath)
{
/*
C:\Users\10139>wmic process where name="notepad.exe" get executablepath,processid
ExecutablePath ProcessId
C:\WINDOWS\system32\notepad.exe 6196
C:\WINDOWS\system32\notepad.exe 6056
C:\Users\10139>taskkill /F /PID 6196 /PID 6056
<20>ɹ<EFBFBD>: <20><><EFBFBD><EFBFBD>ֹ PID Ϊ 6196 <20>Ľ<EFBFBD><C4BD>̡<EFBFBD>
<20>ɹ<EFBFBD>: <20><><EFBFBD><EFBFBD>ֹ PID Ϊ 6056 <20>Ľ<EFBFBD><C4BD>̡<EFBFBD>
*/
if(!boost::filesystem::exists(p_strPath))
{
cout << p_strPath << " not exist" << endl;
return FALSE;
}
int index = p_strPath.rfind("\\");
std::string strName = p_strPath.substr(index + 1);
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;
HANDLE hStdOutRead = NULL, hStdOutWrite = NULL;
if (!CreatePipe(&hStdOutRead, &hStdOutWrite, &sa, 0))
{
cout << "create pipe error," << GetLastError() << endl;
return FALSE;
}
STARTUPINFOA startInfo;
PROCESS_INFORMATION procInfo;
BOOL bSuccess = FALSE;
ZeroMemory(&procInfo, sizeof(PROCESS_INFORMATION));
ZeroMemory(&startInfo, sizeof(STARTUPINFOA));
startInfo.cb = sizeof(STARTUPINFOA);
startInfo.hStdOutput = hStdOutWrite;
startInfo.dwFlags |= (STARTF_USESTDHANDLES |STARTF_USESHOWWINDOW) ;
startInfo.wShowWindow = SW_HIDE;
boost::format fmt("wmic process where name=\"%1%\" get executablepath,processid");
fmt % strName;
std::string strSQL = fmt.str();
bSuccess = CreateProcessA(NULL, (char*)strSQL.data(), NULL, NULL, TRUE, 0, NULL, NULL, &startInfo, &procInfo);
if (!bSuccess)
{
cout << "create process error," << GetLastError() << endl;
return FALSE;
}
WaitForSingleObject(procInfo.hProcess,INFINITE);
CloseHandle(hStdOutWrite);
DWORD byteRead = 0;
std::string strContent;
char buffer[READ_ONE_NUM] = {0};
while (true)
{
byteRead = 0;
memset(buffer, 0, READ_ONE_NUM);
BOOL bRead = ReadFile(hStdOutRead, buffer, (READ_ONE_NUM-1)* sizeof(buffer[0]) , &byteRead, NULL);
if (!bRead)
{
break;
}
strContent.append(buffer);
}
CloseHandle(hStdOutRead);
std::vector<std::string> splitVec;
boost::split(splitVec, strContent, boost::is_any_of("\r\n"), boost::token_compress_on);
if(splitVec.size() > 0)
{
if( !boost::icontains(splitVec[0], "ExecutablePath") )
{
// û<><C3BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
cout << strName << " is not runing" << endl;
return FALSE;
}
// <20><><EFBFBD><EFBFBD>for<6F><72><EFBFBD><EFBFBD><EBA3BA><EFBFBD><EFBFBD><EFBFBD>Ż<EFBFBD>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>·<EFBFBD><C2B7><EFBFBD>ͳ<EFBFBD><CDB3><EFBFBD>PID
// <20><>1<EFBFBD>к<EFBFBD><D0BA><EFBFBD><EFBFBD><EFBFBD>1<EFBFBD>ж<EFBFBD><D0B6><EFBFBD><EFBFBD><EFBFBD>
for(int i = 1; i < splitVec.size() -1; i++)
{
std::vector<std::string> splitVec2;
boost::split(splitVec2, splitVec[i], boost::is_any_of(" "), boost::token_compress_on);
int size = splitVec2.size();
if(size >= 3)
{
std::string exePath;
// ȡ<><C8A1>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>·<EFBFBD><C2B7>
for(int i = 0; i < size -1 -1; i++)
{
exePath.append(splitVec2[i]);
exePath.append(" ");
}
// <20>ж<EFBFBD>·<EFBFBD><C2B7><EFBFBD>Ƿ<EFBFBD><C7B7><EFBFBD>ȫƥ<C8AB><C6A5>
if( !boost::icontains(exePath, p_strPath) )
{
continue;
}
// <20><><EFBFBD><EFBFBD>·<EFBFBD><C2B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>пո񣬵<D5B8><F1A3ACB5><EFBFBD><EFBFBD><EFBFBD>2<EFBFBD><32>Ϊpid
std::string pId = splitVec2[size -1 -1];
std::string cmd = "taskkill /F /PID ";
cmd.append(pId);
cout << p_strPath << "->" << cmd << endl;
WinExec(cmd.c_str(), SW_HIDE);
}
}
}
return TRUE;
}
int main(int argc, char* argv[])
{
for(int i = 1; i < argc ; i++)
{
KillSpecifiedProcess(argv[i]);
}
return 0;
}

674
LICENSE Normal file
View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program 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.
This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

101
README.md Normal file
View File

@@ -0,0 +1,101 @@
# AUTO_MAA
MAA多账号管理与自动化软件
!["软件图标"](https://github.com/DLmaster361/AUTO_MAA/blob/main/resources/images/AUTO_MAA.png "软件图标")
---
</h1>
[![GitHub Stars](https://img.shields.io/github/stars/DLmaster361/AUTO_MAA?style=flat-square)](https://github.com/DLmaster361/AUTO_MAA/stargazers)
[![GitHub Forks](https://img.shields.io/github/forks/DLmaster361/AUTO_MAA?style=flat-square)](https://github.com/DLmaster361/AUTO_MAA/network)
[![GitHub Issues](https://img.shields.io/github/issues/DLmaster361/AUTO_MAA?style=flat-square)](https://github.com/DLmaster361/AUTO_MAA/issues)
[![GitHub Contributors](https://img.shields.io/github/contributors/DLmaster361/AUTO_MAA?style=flat-square)](https://github.com/DLmaster361/AUTO_MAA/graphs/contributors)
[![GitHub License](https://img.shields.io/github/license/DLmaster361/AUTO_MAA?style=flat-square)](https://github.com/DLmaster361/AUTO_MAA/blob/main/LICENSE)
</div>
## 软件介绍
### 性质
本软件是明日方舟第三方软件`MAA`的第三方工具即第3<sup>3</sup>方软件。旨在优化MAA多账号功能体验并通过一些方法解决MAA项目未能解决的部分问题提高代理的稳定性。
### 原理
本软件可以存储多个明日方舟账号数据,并通过以下流程实现代理功能:
1. **配置:** 根据对应用户的配置信息生成配置文件并将其导入MAA。
2. **监测:** 在MAA开始代理后持续读取MAA的日志以判断其运行状态。当软件认定MAA出现异常时通过重启MAA使之仍能继续完成任务。
3. **循环:** 重复上述步骤使MAA依次完成各个用户的自动代理任务。
### 优势
- **节省运行开销:** 只需要一份MAA软件与一个模拟器无需多开就能完成多账号代理羸弱的电脑也能代理日常。
- **自定义空间大:** 依靠高级用户配置模式支持MAA几乎所有设置选项自定义支持模拟器多开。
- **调度方法自由:** 通过调度队列功能自由实现MAA多开等多种调度方式。
- **一键代理无忧:** 无须中途手动修改MAA配置将繁琐交给AUTO_MAA把游戏留给自己。
- **代理结果复核:** 通过人工排查功能核实各用户代理情况,堵住自动代理的最后一丝风险。
## 重要声明
本开发团队承诺不会修改明日方舟游戏本体与相关配置文件。本项目使用GPL开源相关细则如下
- **作者:** AUTO_MAA软件作者为DLmaster、DLmaster361或DLmaster_361以上均指代同一人。
- **使用:** AUTO_MAA使用者可以按自己的意愿自由使用本软件。依据GPL对于由此可能产生的损失AUTO_MAA项目组不负任何责任。
- **分发:** AUTO_MAA允许任何人自由分发本软件包括进行商业活动牟利。若为直接分发本软件必须遵循GPL向接收者提供本软件项目地址、完整的软件源码与GPL协议原文若为修改软件后进行分发必须遵循GPL向接收者提供本软件项目地址、修改前的完整软件源码副本与GPL协议原文违反者可能会被追究法律责任。
- **传播:** AUTO_MAA原则上允许传播者自由传播本软件但无论在何种传播过程中不得删除项目作者与开发者所留版权声明不得隐瞒项目作者与相关开发者的存在。由于软件性质项目组不希望发现任何人在明日方舟官方媒体包括官方媒体账号与森空岛社区等或明日方舟游戏相关内容包括同好群、线下活动与游戏内容讨论等下提及AUTO_MAA或MAA希望各位理解。
- **衍生:** AUTO_MAA允许任何人对软件本体或软件部分代码进行二次开发或利用。但依据GPL相关成果再次分发时也必须使用GPL或兼容的协议开源。
- **贡献:** 不论是直接参与软件的维护编写或是撰写文档、测试、反馈BUG、给出建议、参与讨论都为AUTO_MAA项目的发展完善做出了不可忽视的贡献。项目组提倡各位贡献者遵照GitHub开源社区惯例发布Issues参与项目。避免私信或私发邮件安全性漏洞或敏感问题除外以帮助更多用户。
以上细则是本项目对GPL的相关补充与强调。未提及的以GPL为准发生冲突的以本细则为准。如有不清楚的部分请发Issues询问。若发生纠纷相关内容也没有在Issues上提及的项目组拥有最终解释权。
**注意**
- 由于本软件有修改其它目录JSON文件等行为使用前请将AUTO_MAA添加入Windows Defender信任区以及防病毒软件的信任区或开发者目录避免被误杀。
---
# 使用方法
访问AUTO_MAA官方文档站以获取使用指南和项目相关信息
- [AUTO_MAA官方文档站](https://clozya.github.io/AUTOMAA_docs)
---
# 关于
## 项目开发情况
可在[《AUTO_MAA开发者协作文档》](https://docs.qq.com/aio/DQ3Z5eHNxdmxFQmZX)的`开发任务`页面中查看开发进度。
## 贡献者
感谢以下贡献者对本项目做出的贡献
<a href="https://github.com/DLmaster361/AUTO_MAA/graphs/contributors">
<img src="https://contrib.rocks/image?repo=DLmaster361/AUTO_MAA" />
</a>
![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)
## 交流与赞助
欢迎加入AUTO_MAA项目组欢迎反馈bug
- QQ交流群[957750551](https://qm.qq.com/q/bd9fISNoME)
---
如果喜欢这个项目的话,给作者来杯咖啡吧!
![payid](https://github.com/DLmaster361/AUTO_MAA/blob/main/resources/README/payid.png "payid")

51
app/__init__.py Normal file
View File

@@ -0,0 +1,51 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA主程序包
v4.2
作者DLmaster_361
"""
__version__ = "4.2.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .core import AppConfig, QueueConfig, MaaConfig, Task, TaskManager, MainTimer
from .models import MaaManager
from .services import Notify, Crypto, System
from .ui import AUTO_MAA
from .utils import Updater
__all__ = [
"AppConfig",
"QueueConfig",
"MaaConfig",
"Task",
"TaskManager",
"MainTimer",
"MaaManager",
"Notify",
"Crypto",
"System",
"AUTO_MAA",
"Updater",
]

46
app/core/__init__.py Normal file
View File

@@ -0,0 +1,46 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA核心组件包
v4.2
作者DLmaster_361
"""
__version__ = "4.2.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .config import AppConfig, QueueConfig, MaaConfig, Config
from .main_info_bar import MainInfoBar
from .task_manager import Task, TaskManager
from .timer import MainTimer
__all__ = [
"AppConfig",
"Config",
"QueueConfig",
"MaaConfig",
"MainInfoBar",
"Task",
"TaskManager",
"MainTimer",
]

944
app/core/config.py Normal file
View File

@@ -0,0 +1,944 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA配置管理
v4.2
作者DLmaster_361
"""
from loguru import logger
import sqlite3
import json
import sys
import shutil
import re
from datetime import datetime
from collections import defaultdict
from pathlib import Path
from qfluentwidgets import (
QConfig,
ConfigItem,
OptionsConfigItem,
RangeConfigItem,
FolderValidator,
BoolValidator,
RangeValidator,
OptionsValidator,
qconfig,
)
from typing import Union, Dict, List, Tuple
class AppConfig:
def __init__(self) -> None:
self.app_path = Path(sys.argv[0]).resolve().parent # 获取软件根目录
self.app_path_sys = str(Path(sys.argv[0]).resolve()) # 获取软件自身的路径
self.log_path = self.app_path / "debug/AUTO_MAA.log"
self.database_path = self.app_path / "data/data.db"
self.config_path = self.app_path / "config/config.json"
self.history_path = self.app_path / "history/main.json"
self.key_path = self.app_path / "data/key"
self.gameid_path = self.app_path / "data/gameid.txt"
self.version_path = self.app_path / "resources/version.json"
self.PASSWORD = ""
self.running_list = []
self.silence_list = []
self.if_database_opened = False
# 检查文件完整性
self.initialize()
def initialize(self) -> None:
"""初始化程序的配置文件"""
# 检查目录
(self.app_path / "config").mkdir(parents=True, exist_ok=True)
(self.app_path / "data").mkdir(parents=True, exist_ok=True)
(self.app_path / "debug").mkdir(parents=True, exist_ok=True)
(self.app_path / "history").mkdir(parents=True, exist_ok=True)
# 生成版本信息文件
if not self.version_path.exists():
version = {
"main_version": "0.0.0.0",
"updater_version": "0.0.0.0",
}
with self.version_path.open(mode="w", encoding="utf-8") as f:
json.dump(version, f, ensure_ascii=False, indent=4)
# 生成预设gameid替换方案文件
if not self.gameid_path.exists():
self.gameid_path.write_text(
"龙门币CE-6\n技能CA-5\n红票AP-5\n经验LS-6\n剿灭模式Annihilation",
encoding="utf-8",
)
self.init_logger()
self.init_config()
self.check_data()
logger.info("程序配置管理模块初始化完成")
def init_logger(self) -> None:
"""初始化日志记录器"""
logger.remove(0)
logger.add(
sink=self.log_path,
level="DEBUG",
format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <level>{message}</level>",
enqueue=True,
backtrace=True,
diagnose=True,
rotation="1 week",
retention="1 month",
compression="zip",
)
logger.info("===================================")
logger.info("AUTO_MAA 主程序")
logger.info("版本号: v4.2.1.1")
logger.info(f"根目录: {self.app_path}")
logger.info("===================================")
logger.info("日志记录器初始化完成")
def init_config(self) -> None:
"""初始化配置类"""
self.global_config = GlobalConfig()
self.queue_config = QueueConfig()
self.maa_config = MaaConfig()
qconfig.load(self.config_path, self.global_config)
config_list = self.search_config()
for config in config_list:
if config[0] == "Maa":
qconfig.load(config[1], self.maa_config)
self.maa_config.save()
elif config[0] == "Queue":
qconfig.load(config[1], self.queue_config)
self.queue_config.save()
logger.info("配置类初始化完成")
def init_database(self, mode: str) -> None:
"""初始化用户数据库"""
if mode == "Maa":
self.cur.execute(
"CREATE TABLE adminx(admin text,id text,server text,day int,status text,last date,game text,game_1 text,game_2 text,routine text,annihilation text,infrastructure text,password byte,notes text,numb int,mode text,uid int)"
)
self.cur.execute("CREATE TABLE version(v text)")
self.cur.execute("INSERT INTO version VALUES(?)", ("v1.4",))
self.db.commit()
logger.info("用户数据库初始化完成")
def check_data(self) -> None:
"""检查用户数据文件并处理数据文件版本更新"""
# 生成主数据库
if not self.database_path.exists():
db = sqlite3.connect(self.database_path)
cur = db.cursor()
cur.execute("CREATE TABLE version(v text)")
cur.execute("INSERT INTO version VALUES(?)", ("v1.4",))
db.commit()
cur.close()
db.close()
# 数据文件版本更新
db = sqlite3.connect(self.database_path)
cur = db.cursor()
cur.execute("SELECT * FROM version WHERE True")
version = cur.fetchall()
if version[0][0] != "v1.4":
logger.info("数据文件版本更新开始")
if_streaming = False
# v1.0-->v1.1
if version[0][0] == "v1.0" or if_streaming:
logger.info("数据文件版本更新v1.0-->v1.1")
if_streaming = True
cur.execute("SELECT * FROM adminx WHERE True")
data = cur.fetchall()
cur.execute("DROP TABLE IF EXISTS adminx")
cur.execute(
"CREATE TABLE adminx(admin text,id text,server text,day int,status text,last date,game text,game_1 text,game_2 text,routines text,annihilation text,infrastructure text,password byte,notes text,numb int,mode text,uid int)"
)
for i in range(len(data)):
cur.execute(
"INSERT INTO adminx VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
data[i][0], # 0 0 0
data[i][1], # 1 1 -
"Official", # 2 2 -
data[i][2], # 3 3 1
data[i][3], # 4 4 2
data[i][4], # 5 5 3
data[i][5], # 6 6 -
data[i][6], # 7 7 -
data[i][7], # 8 8 -
"y", # 9 - 4
data[i][8], # 10 9 5
data[i][9], # 11 10 -
data[i][10], # 12 11 6
data[i][11], # 13 12 7
data[i][12], # 14 - -
"simple", # 15 - -
data[i][13], # 16 - -
),
)
cur.execute("DELETE FROM version WHERE v = ?", ("v1.0",))
cur.execute("INSERT INTO version VALUES(?)", ("v1.1",))
db.commit()
# v1.1-->v1.2
if version[0][0] == "v1.1" or if_streaming:
logger.info("数据文件版本更新v1.1-->v1.2")
if_streaming = True
cur.execute("SELECT * FROM adminx WHERE True")
data = cur.fetchall()
for i in range(len(data)):
cur.execute(
"UPDATE adminx SET infrastructure = 'n' WHERE mode = ? AND uid = ?",
(
data[i][15],
data[i][16],
),
)
cur.execute("DELETE FROM version WHERE v = ?", ("v1.1",))
cur.execute("INSERT INTO version VALUES(?)", ("v1.2",))
db.commit()
# v1.2-->v1.3
if version[0][0] == "v1.2" or if_streaming:
logger.info("数据文件版本更新v1.2-->v1.3")
if_streaming = True
cur.execute("ALTER TABLE adminx RENAME COLUMN routines TO routine")
cur.execute("DELETE FROM version WHERE v = ?", ("v1.2",))
cur.execute("INSERT INTO version VALUES(?)", ("v1.3",))
db.commit()
# v1.3-->v1.4
if version[0][0] == "v1.3" or if_streaming:
logger.info("数据文件版本更新v1.3-->v1.4")
if_streaming = True
(self.app_path / "config/MaaConfig").mkdir(parents=True, exist_ok=True)
shutil.move(
self.app_path / "data/MaaConfig",
self.app_path / "config/MaaConfig",
)
(self.app_path / "config/MaaConfig/MaaConfig").rename(
self.app_path / "config/MaaConfig/脚本_1"
)
shutil.copy(
self.database_path,
self.app_path / "config/MaaConfig/脚本_1/user_data.db",
)
cur.execute("DROP TABLE IF EXISTS adminx")
cur.execute("DELETE FROM version WHERE v = ?", ("v1.3",))
cur.execute("INSERT INTO version VALUES(?)", ("v1.4",))
db.commit()
with (self.app_path / "config/gui.json").open(
"r", encoding="utf-8"
) as f:
info = json.load(f)
maa_config = {
"MaaSet": {
"Name": "",
"Path": info["Default"]["MaaSet.path"],
},
"RunSet": {
"AnnihilationTimeLimit": info["Default"][
"TimeLimit.annihilation"
],
"RoutineTimeLimit": info["Default"]["TimeLimit.routine"],
"RunTimesLimit": info["Default"]["TimesLimit.run"],
},
}
with (self.app_path / "config/MaaConfig/脚本_1/config.json").open(
"w", encoding="utf-8"
) as f:
json.dump(maa_config, f, ensure_ascii=False, indent=4)
config = {
"Function": {
"BossKey": info["Default"]["SelfSet.BossKey"],
"IfAllowSleep": bool(
info["Default"]["SelfSet.IfSleep"] == "True"
),
"IfSilence": bool(
info["Default"]["SelfSet.IfSilence"] == "True"
),
},
"Notify": {
"IfPushPlyer": True,
"IfSendErrorOnly": bool(
info["Default"]["SelfSet.IfSendMail.OnlyError"] == "True"
),
"IfSendMail": bool(
info["Default"]["SelfSet.IfSendMail"] == "True"
),
"MailAddress": info["Default"]["SelfSet.MailAddress"],
},
"Start": {
"IfRunDirectly": bool(
info["Default"]["SelfSet.IfProxyDirectly"] == "True"
),
"IfSelfStart": bool(
info["Default"]["SelfSet.IfSelfStart"] == "True"
),
},
"UI": {
"IfShowTray": bool(
info["Default"]["SelfSet.IfToTray"] == "True"
),
"IfToTray": bool(info["Default"]["SelfSet.IfToTray"] == "True"),
"location": info["Default"]["SelfSet.UIlocation"],
"maximized": bool(
info["Default"]["SelfSet.UImaximized"] == "True"
),
"size": info["Default"]["SelfSet.UIsize"],
},
"Update": {"IfAutoUpdate": False},
}
with (self.app_path / "config/config.json").open(
"w", encoding="utf-8"
) as f:
json.dump(config, f, ensure_ascii=False, indent=4)
queue_config = {
"QueueSet": {"Enabled": True, "Name": ""},
"Queue": {
"Member_1": "脚本_1",
"Member_10": "禁用",
"Member_2": "禁用",
"Member_3": "禁用",
"Member_4": "禁用",
"Member_5": "禁用",
"Member_6": "禁用",
"Member_7": "禁用",
"Member_8": "禁用",
"Member_9": "禁用",
},
"Time": {
"TimeEnabled_0": bool(
info["Default"]["TimeSet.set1"] == "True"
),
"TimeEnabled_1": bool(
info["Default"]["TimeSet.set2"] == "True"
),
"TimeEnabled_2": bool(
info["Default"]["TimeSet.set3"] == "True"
),
"TimeEnabled_3": bool(
info["Default"]["TimeSet.set4"] == "True"
),
"TimeEnabled_4": bool(
info["Default"]["TimeSet.set5"] == "True"
),
"TimeEnabled_5": bool(
info["Default"]["TimeSet.set6"] == "True"
),
"TimeEnabled_6": bool(
info["Default"]["TimeSet.set7"] == "True"
),
"TimeEnabled_7": bool(
info["Default"]["TimeSet.set8"] == "True"
),
"TimeEnabled_8": bool(
info["Default"]["TimeSet.set9"] == "True"
),
"TimeEnabled_9": bool(
info["Default"]["TimeSet.set10"] == "True"
),
"TimeSet_0": info["Default"]["TimeSet.run1"],
"TimeSet_1": info["Default"]["TimeSet.run2"],
"TimeSet_2": info["Default"]["TimeSet.run3"],
"TimeSet_3": info["Default"]["TimeSet.run4"],
"TimeSet_4": info["Default"]["TimeSet.run5"],
"TimeSet_5": info["Default"]["TimeSet.run6"],
"TimeSet_6": info["Default"]["TimeSet.run7"],
"TimeSet_7": info["Default"]["TimeSet.run8"],
"TimeSet_8": info["Default"]["TimeSet.run9"],
"TimeSet_9": info["Default"]["TimeSet.run10"],
},
}
(self.app_path / "config/QueueConfig").mkdir(
parents=True, exist_ok=True
)
with (self.app_path / "config/QueueConfig/调度队列_1.json").open(
"w", encoding="utf-8"
) as f:
json.dump(queue_config, f, ensure_ascii=False, indent=4)
(self.app_path / "config/gui.json").unlink()
cur.close()
db.close()
logger.info("数据文件版本更新完成")
def search_config(self) -> list:
"""搜索所有子配置文件"""
config_list = []
if (self.app_path / "config/MaaConfig").exists():
for subdir in (self.app_path / "config/MaaConfig").iterdir():
if subdir.is_dir():
config_list.append(["Maa", subdir / "config.json"])
if (self.app_path / "config/QueueConfig").exists():
for json_file in (self.app_path / "config/QueueConfig").glob("*.json"):
config_list.append(["Queue", json_file])
return config_list
def open_database(self, mode: str, index: str = None) -> None:
"""打开数据库"""
self.close_database()
self.db = sqlite3.connect(
self.app_path / f"config/{mode}Config/{index}/user_data.db"
)
self.cur = self.db.cursor()
self.if_database_opened = True
def close_database(self) -> None:
"""关闭数据库"""
if self.if_database_opened:
self.cur.close()
self.db.close()
self.if_database_opened = False
def change_user_info(
self,
data_path: Path,
modes: list,
uids: list,
days: list,
lasts: list,
notes: list,
numbs: list,
) -> None:
"""将代理完成后发生改动的用户信息同步至本地数据库"""
db = sqlite3.connect(data_path / "user_data.db")
cur = db.cursor()
for index in range(len(uids)):
cur.execute(
"UPDATE adminx SET day = ? WHERE mode = ? AND uid = ?",
(days[index], modes[index], uids[index]),
)
cur.execute(
"UPDATE adminx SET last = ? WHERE mode = ? AND uid = ?",
(lasts[index], modes[index], uids[index]),
)
cur.execute(
"UPDATE adminx SET notes = ? WHERE mode = ? AND uid = ?",
(notes[index], modes[index], uids[index]),
)
cur.execute(
"UPDATE adminx SET numb = ? WHERE mode = ? AND uid = ?",
(numbs[index], modes[index], uids[index]),
)
db.commit()
cur.close()
db.close()
def save_maa_log(self, log_path: Path, logs: list, maa_result: str) -> bool:
"""保存MAA日志"""
data: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = {
"recruit_statistics": defaultdict(int),
"drop_statistics": defaultdict(dict),
"maa_result": maa_result,
}
if_six_star = False
# 公招统计(仅统计招募到的)
confirmed_recruit = False
current_star_level = None
i = 0
while i < len(logs):
if "公招识别结果:" in logs[i]:
current_star_level = None # 每次识别公招时清空之前的星级
i += 1
while i < len(logs) and "Tags" not in logs[i]: # 读取所有公招标签
i += 1
if i < len(logs) and "Tags" in logs[i]: # 识别星级
star_match = re.search(r"(\d+)\s*★ Tags", logs[i])
if star_match:
current_star_level = f"{star_match.group(1)}"
if current_star_level == "6★":
if_six_star = True
if "已确认招募" in logs[i]: # 只有确认招募后才统计
confirmed_recruit = True
if confirmed_recruit and current_star_level:
data["recruit_statistics"][current_star_level] += 1
confirmed_recruit = False # 重置,等待下一次公招
current_star_level = None # 清空已处理的星级
i += 1
# 掉落统计
# 存储所有关卡的掉落统计
all_stage_drops = {}
# 查找所有Fight任务的开始和结束位置
fight_tasks = []
for i, line in enumerate(logs):
if "开始任务: Fight" in line:
# 查找对应的任务结束位置
end_index = -1
for j in range(i+1, len(logs)):
if "完成任务: Fight" in logs[j]:
end_index = j
break
# 如果遇到新的Fight任务开始则当前任务没有正常结束
if j < len(logs) and "开始任务: Fight" in logs[j]:
break
# 如果找到了结束位置,记录这个任务的范围
if end_index != -1:
fight_tasks.append((i, end_index))
# 处理每个Fight任务
for start_idx, end_idx in fight_tasks:
# 提取当前任务的日志
task_logs = logs[start_idx:end_idx+1]
# 查找任务中的最后一次掉落统计
last_drop_stats = {}
current_stage = None
for line in task_logs:
# 匹配掉落统计行,如"1-7 掉落统计:"
drop_match = re.search(r"([A-Za-z0-9\-]+) 掉落统计:", line)
if drop_match:
# 发现新的掉落统计,重置当前关卡的掉落数据
current_stage = drop_match.group(1)
last_drop_stats = {}
continue
# 如果已经找到了关卡,处理掉落物
if current_stage:
item_match: List[str] = re.findall(
r"^(?!\[)([\u4e00-\u9fa5A-Za-z0-9\-]+)\s*:\s*([\d,]+)(?:\s*\(\+[\d,]+\))?",
line,
re.M,
)
for item, total in item_match:
# 解析数值时去掉逗号 (如 2,160 -> 2160
total = int(total.replace(",", ""))
# 黑名单
if item not in [
"当前次数",
"理智",
"最快截图耗时",
"专精等级",
"剩余时间",
]:
last_drop_stats[item] = total
# 如果任务中有掉落统计,更新总统计
if current_stage and last_drop_stats:
if current_stage not in all_stage_drops:
all_stage_drops[current_stage] = {}
# 累加掉落数据
for item, count in last_drop_stats.items():
all_stage_drops[current_stage].setdefault(item, 0)
all_stage_drops[current_stage][item] += count
# 将累加后的掉落数据保存到结果中
data["drop_statistics"] = all_stage_drops
# 保存日志
log_path.parent.mkdir(parents=True, exist_ok=True)
with log_path.open("w", encoding="utf-8") as f:
f.writelines(logs)
with log_path.with_suffix(".json").open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
logger.info(f"处理完成:{log_path}")
self.merge_maa_logs("所有项", log_path.parent)
return if_six_star
def merge_maa_logs(self, mode: str, logs_path: Union[Path, List[Path]]) -> dict:
"""合并指定数据统计信息文件"""
data = {
"recruit_statistics": defaultdict(int),
"drop_statistics": defaultdict(dict),
"maa_result": defaultdict(str),
}
if mode == "所有项":
logs_path_list = list(logs_path.glob("*.json"))
elif mode == "指定项":
logs_path_list = logs_path
for json_file in logs_path_list:
with json_file.open("r", encoding="utf-8") as f:
single_data: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = (
json.load(f)
)
# 合并公招统计
for star_level, count in single_data["recruit_statistics"].items():
data["recruit_statistics"][star_level] += count
# 合并掉落统计
for stage, drops in single_data["drop_statistics"].items():
if stage not in data["drop_statistics"]:
data["drop_statistics"][stage] = {} # 初始化关卡
for item, count in drops.items():
if item in data["drop_statistics"][stage]:
data["drop_statistics"][stage][item] += count
else:
data["drop_statistics"][stage][item] = count
# 合并MAA结果
data["maa_result"][
json_file.name.replace(".json", "").replace("-", ":")
] = single_data["maa_result"]
# 生成汇总 JSON 文件
if mode == "所有项":
with logs_path.with_suffix(".json").open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
logger.info(f"统计完成:{logs_path.with_suffix('.json')}")
return data
def load_maa_logs(
self, mode: str, json_path: Path
) -> Dict[str, Union[str, list, Dict[str, list]]]:
"""加载MAA日志统计信息"""
if mode == "总览":
with json_path.open("r", encoding="utf-8") as f:
info: Dict[str, Dict[str, Union[int, dict]]] = json.load(f)
data = {}
data["条目索引"] = [
[k, "完成" if v == "Success!" else "异常"]
for k, v in info["maa_result"].items()
]
data["条目索引"].insert(0, ["数据总览", "运行"])
data["统计数据"] = {"公招统计": list(info["recruit_statistics"].items())}
for game_id, drops in info["drop_statistics"].items():
data["统计数据"][f"掉落统计:{game_id}"] = list(drops.items())
data["统计数据"]["报错汇总"] = [
[k, v] for k, v in info["maa_result"].items() if v != "Success!"
]
elif mode == "单项":
with json_path.open("r", encoding="utf-8") as f:
info: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = json.load(f)
data = {}
data["统计数据"] = {"公招统计": list(info["recruit_statistics"].items())}
for game_id, drops in info["drop_statistics"].items():
data["统计数据"][f"掉落统计:{game_id}"] = list(drops.items())
with json_path.with_suffix(".log").open("r", encoding="utf-8") as f:
log = f.read()
data["日志信息"] = log
return data
def search_history(self) -> dict:
"""搜索所有历史记录"""
history_dict = {}
for date_folder in (Config.app_path / "history").iterdir():
if not date_folder.is_dir():
continue # 只处理日期文件夹
try:
date = datetime.strptime(date_folder.name, "%Y-%m-%d")
history_dict[date.strftime("%Y年 %m月 %d")] = list(
date_folder.glob("*.json")
)
except ValueError:
logger.warning(f"非日期格式的目录: {date_folder}")
return {
k: v
for k, v in sorted(
history_dict.items(),
key=lambda x: datetime.strptime(x[0], "%Y年 %m月 %d"),
reverse=True,
)
}
def save_history(self, key: str, content: dict) -> None:
"""保存历史记录"""
history = {}
if self.history_path.exists():
with self.history_path.open(mode="r", encoding="utf-8") as f:
history = json.load(f)
history[key] = content
with self.history_path.open(mode="w", encoding="utf-8") as f:
json.dump(history, f, ensure_ascii=False, indent=4)
def get_history(self, key: str) -> dict:
"""获取历史记录"""
history = {}
if self.history_path.exists():
with self.history_path.open(mode="r", encoding="utf-8") as f:
history = json.load(f)
return history.get(
key, {"Time": "0000-00-00 00:00", "History": "暂无历史运行记录"}
)
def clear_maa_config(self) -> None:
"""清空MAA配置"""
self.maa_config.set(self.maa_config.MaaSet_Name, "")
self.maa_config.set(self.maa_config.MaaSet_Path, ".")
self.maa_config.set(self.maa_config.RunSet_TaskTransitionMethod, "ExitEmulator")
self.maa_config.set(self.maa_config.RunSet_ProxyTimesLimit, 0)
self.maa_config.set(self.maa_config.RunSet_AnnihilationTimeLimit, 40)
self.maa_config.set(self.maa_config.RunSet_RoutineTimeLimit, 10)
self.maa_config.set(self.maa_config.RunSet_RunTimesLimit, 3)
self.maa_config.set(self.maa_config.MaaSet_Name, "")
self.maa_config.set(self.maa_config.MaaSet_Name, "")
self.maa_config.set(self.maa_config.MaaSet_Name, "")
def clear_queue_config(self) -> None:
"""清空队列配置"""
self.queue_config.set(self.queue_config.queueSet_Name, "")
self.queue_config.set(self.queue_config.queueSet_Enabled, False)
self.queue_config.set(self.queue_config.queueSet_AfterAccomplish, "None")
self.queue_config.set(self.queue_config.time_TimeEnabled_0, False)
self.queue_config.set(self.queue_config.time_TimeSet_0, "00:00")
self.queue_config.set(self.queue_config.time_TimeEnabled_1, False)
self.queue_config.set(self.queue_config.time_TimeSet_1, "00:00")
self.queue_config.set(self.queue_config.time_TimeEnabled_2, False)
self.queue_config.set(self.queue_config.time_TimeSet_2, "00:00")
self.queue_config.set(self.queue_config.time_TimeEnabled_3, False)
self.queue_config.set(self.queue_config.time_TimeSet_3, "00:00")
self.queue_config.set(self.queue_config.time_TimeEnabled_4, False)
self.queue_config.set(self.queue_config.time_TimeSet_4, "00:00")
self.queue_config.set(self.queue_config.time_TimeEnabled_5, False)
self.queue_config.set(self.queue_config.time_TimeSet_5, "00:00")
self.queue_config.set(self.queue_config.time_TimeEnabled_6, False)
self.queue_config.set(self.queue_config.time_TimeSet_6, "00:00")
self.queue_config.set(self.queue_config.time_TimeEnabled_7, False)
self.queue_config.set(self.queue_config.time_TimeSet_7, "00:00")
self.queue_config.set(self.queue_config.time_TimeEnabled_8, False)
self.queue_config.set(self.queue_config.time_TimeSet_8, "00:00")
self.queue_config.set(self.queue_config.time_TimeEnabled_9, False)
self.queue_config.set(self.queue_config.time_TimeSet_9, "00:00")
self.queue_config.set(self.queue_config.queue_Member_1, "禁用")
self.queue_config.set(self.queue_config.queue_Member_2, "禁用")
self.queue_config.set(self.queue_config.queue_Member_3, "禁用")
self.queue_config.set(self.queue_config.queue_Member_4, "禁用")
self.queue_config.set(self.queue_config.queue_Member_5, "禁用")
self.queue_config.set(self.queue_config.queue_Member_6, "禁用")
self.queue_config.set(self.queue_config.queue_Member_7, "禁用")
self.queue_config.set(self.queue_config.queue_Member_8, "禁用")
self.queue_config.set(self.queue_config.queue_Member_9, "禁用")
self.queue_config.set(self.queue_config.queue_Member_10, "禁用")
class GlobalConfig(QConfig):
"""全局配置"""
function_HomeImageMode = OptionsConfigItem(
"Function",
"HomeImageMode",
"默认",
OptionsValidator(["默认", "自定义", "主题图像"]),
)
function_HistoryRetentionTime = OptionsConfigItem(
"Function", "HistoryRetentionTime", 0, OptionsValidator([7, 15, 30, 60, 0])
)
function_IfAllowSleep = ConfigItem(
"Function", "IfAllowSleep", False, BoolValidator()
)
function_IfSilence = ConfigItem("Function", "IfSilence", False, BoolValidator())
function_BossKey = ConfigItem("Function", "BossKey", "")
function_IfAgreeBilibili = ConfigItem(
"Function", "IfAgreeBilibili", False, BoolValidator()
)
start_IfSelfStart = ConfigItem("Start", "IfSelfStart", False, BoolValidator())
start_IfRunDirectly = ConfigItem("Start", "IfRunDirectly", False, BoolValidator())
start_IfMinimizeDirectly = ConfigItem(
"Start", "IfMinimizeDirectly", False, BoolValidator()
)
ui_IfShowTray = ConfigItem("UI", "IfShowTray", False, BoolValidator())
ui_IfToTray = ConfigItem("UI", "IfToTray", False, BoolValidator())
ui_size = ConfigItem("UI", "size", "1200x700")
ui_location = ConfigItem("UI", "location", "100x100")
ui_maximized = ConfigItem("UI", "maximized", False, BoolValidator())
notify_SendTaskResultTime = OptionsConfigItem(
"Notify",
"SendTaskResultTime",
"不推送",
OptionsValidator(["不推送", "任何时刻", "仅失败时"]),
)
notify_IfSendStatistic = ConfigItem(
"Notify", "IfSendStatistic", False, BoolValidator()
)
notify_IfSendSixStar = ConfigItem("Notify", "IfSendSixStar", False, BoolValidator())
notify_IfPushPlyer = ConfigItem("Notify", "IfPushPlyer", False, BoolValidator())
notify_IfSendMail = ConfigItem("Notify", "IfSendMail", False, BoolValidator())
notify_SMTPServerAddress = ConfigItem("Notify", "SMTPServerAddress", "")
notify_AuthorizationCode = ConfigItem("Notify", "AuthorizationCode", "")
notify_FromAddress = ConfigItem("Notify", "FromAddress", "")
notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
notify_IfServerChan = ConfigItem("Notify", "IfServerChan", False, BoolValidator())
notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
notify_ServerChanChannel = ConfigItem("Notify", "ServerChanChannel", "")
notify_ServerChanTag = ConfigItem("Notify", "ServerChanTag", "")
notify_IfCompanyWebHookBot = ConfigItem(
"Notify", "IfCompanyWebHookBot", False, BoolValidator()
)
notify_CompanyWebHookBotUrl = ConfigItem("Notify", "CompanyWebHookBotUrl", "")
notify_IfPushDeer = ConfigItem("Notify", "IfPushDeer", False, BoolValidator())
notify_IfPushDeerKey = ConfigItem("Notify", "PushDeerKey", "")
update_IfAutoUpdate = ConfigItem("Update", "IfAutoUpdate", False, BoolValidator())
update_UpdateType = OptionsConfigItem(
"Update", "UpdateType", "main", OptionsValidator(["main", "dev"])
)
class QueueConfig(QConfig):
"""队列配置"""
queueSet_Name = ConfigItem("QueueSet", "Name", "")
queueSet_Enabled = ConfigItem("QueueSet", "Enabled", False, BoolValidator())
queueSet_AfterAccomplish = OptionsConfigItem(
"QueueSet",
"AfterAccomplish",
"None",
OptionsValidator(["None", "KillSelf", "Sleep", "Hibernate", "Shutdown"]),
)
time_TimeEnabled_0 = ConfigItem("Time", "TimeEnabled_0", False, BoolValidator())
time_TimeSet_0 = ConfigItem("Time", "TimeSet_0", "00:00")
time_TimeEnabled_1 = ConfigItem("Time", "TimeEnabled_1", False, BoolValidator())
time_TimeSet_1 = ConfigItem("Time", "TimeSet_1", "00:00")
time_TimeEnabled_2 = ConfigItem("Time", "TimeEnabled_2", False, BoolValidator())
time_TimeSet_2 = ConfigItem("Time", "TimeSet_2", "00:00")
time_TimeEnabled_3 = ConfigItem("Time", "TimeEnabled_3", False, BoolValidator())
time_TimeSet_3 = ConfigItem("Time", "TimeSet_3", "00:00")
time_TimeEnabled_4 = ConfigItem("Time", "TimeEnabled_4", False, BoolValidator())
time_TimeSet_4 = ConfigItem("Time", "TimeSet_4", "00:00")
time_TimeEnabled_5 = ConfigItem("Time", "TimeEnabled_5", False, BoolValidator())
time_TimeSet_5 = ConfigItem("Time", "TimeSet_5", "00:00")
time_TimeEnabled_6 = ConfigItem("Time", "TimeEnabled_6", False, BoolValidator())
time_TimeSet_6 = ConfigItem("Time", "TimeSet_6", "00:00")
time_TimeEnabled_7 = ConfigItem("Time", "TimeEnabled_7", False, BoolValidator())
time_TimeSet_7 = ConfigItem("Time", "TimeSet_7", "00:00")
time_TimeEnabled_8 = ConfigItem("Time", "TimeEnabled_8", False, BoolValidator())
time_TimeSet_8 = ConfigItem("Time", "TimeSet_8", "00:00")
time_TimeEnabled_9 = ConfigItem("Time", "TimeEnabled_9", False, BoolValidator())
time_TimeSet_9 = ConfigItem("Time", "TimeSet_9", "00:00")
queue_Member_1 = OptionsConfigItem("Queue", "Member_1", "禁用")
queue_Member_2 = OptionsConfigItem("Queue", "Member_2", "禁用")
queue_Member_3 = OptionsConfigItem("Queue", "Member_3", "禁用")
queue_Member_4 = OptionsConfigItem("Queue", "Member_4", "禁用")
queue_Member_5 = OptionsConfigItem("Queue", "Member_5", "禁用")
queue_Member_6 = OptionsConfigItem("Queue", "Member_6", "禁用")
queue_Member_7 = OptionsConfigItem("Queue", "Member_7", "禁用")
queue_Member_8 = OptionsConfigItem("Queue", "Member_8", "禁用")
queue_Member_9 = OptionsConfigItem("Queue", "Member_9", "禁用")
queue_Member_10 = OptionsConfigItem("Queue", "Member_10", "禁用")
class MaaConfig(QConfig):
"""MAA配置"""
MaaSet_Name = ConfigItem("MaaSet", "Name", "")
MaaSet_Path = ConfigItem("MaaSet", "Path", ".", FolderValidator())
RunSet_TaskTransitionMethod = OptionsConfigItem(
"RunSet",
"TaskTransitionMethod",
"ExitEmulator",
OptionsValidator(["NoAction", "ExitGame", "ExitEmulator"]),
)
RunSet_ProxyTimesLimit = RangeConfigItem(
"RunSet", "ProxyTimesLimit", 0, RangeValidator(0, 1024)
)
RunSet_AnnihilationTimeLimit = RangeConfigItem(
"RunSet", "AnnihilationTimeLimit", 40, RangeValidator(1, 1024)
)
RunSet_RoutineTimeLimit = RangeConfigItem(
"RunSet", "RoutineTimeLimit", 10, RangeValidator(1, 1024)
)
RunSet_RunTimesLimit = RangeConfigItem(
"RunSet", "RunTimesLimit", 3, RangeValidator(1, 1024)
)
Config = AppConfig()

92
app/core/main_info_bar.py Normal file
View File

@@ -0,0 +1,92 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA信息通知栏
v4.2
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtCore import Qt
from qfluentwidgets import (
InfoBar,
InfoBarPosition,
)
class _MainInfoBar:
"""信息通知栏"""
def __init__(self, main_window=None):
self.main_window = main_window
def push_info_bar(self, mode: str, title: str, content: str, time: int):
"""推送到信息通知栏"""
if self.main_window is None:
logger.error("信息通知栏未设置父窗口")
return None
if mode == "success":
InfoBar.success(
title=title,
content=content,
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=time,
parent=self.main_window,
)
elif mode == "warning":
InfoBar.warning(
title=title,
content=content,
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=time,
parent=self.main_window,
)
elif mode == "error":
InfoBar.error(
title=title,
content=content,
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=time,
parent=self.main_window,
)
elif mode == "info":
InfoBar.info(
title=title,
content=content,
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=time,
parent=self.main_window,
)
MainInfoBar = _MainInfoBar()

296
app/core/task_manager.py Normal file
View File

@@ -0,0 +1,296 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA业务调度器
v4.2
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtCore import QThread, QObject, Signal
from qfluentwidgets import Dialog
import json
from pathlib import Path
from datetime import datetime
from typing import Dict, Union
from .config import Config
from .main_info_bar import MainInfoBar
from app.models import MaaManager
from app.services import System
class Task(QThread):
"""业务线程"""
push_info_bar = Signal(str, str, str, int)
question = Signal(str, str)
question_response = Signal(bool)
update_user_info = Signal(Path, list, list, list, list, list, list)
create_task_list = Signal(list)
create_user_list = Signal(list)
update_task_list = Signal(list)
update_user_list = Signal(list)
update_log_text = Signal(str)
accomplish = Signal(list)
def __init__(
self, mode: str, name: str, info: Dict[str, Dict[str, Union[str, int, bool]]]
):
super(Task, self).__init__()
self.mode = mode
self.name = name
self.info = info
self.logs = []
self.question_response.connect(lambda: print("response"))
def run(self):
if "设置MAA" in self.mode:
logger.info(f"任务开始:设置{self.name}")
self.push_info_bar.emit("info", "设置MAA", self.name, 3000)
self.task = MaaManager(
self.mode,
Config.app_path / f"config/MaaConfig/{self.name}",
(
None
if "全局" in self.mode
else Config.app_path
/ f"config/MaaConfig/{self.name}/beta/{self.info["SetMaaInfo"]["UserId"]}/{self.info["SetMaaInfo"]["SetType"]}"
),
)
self.task.push_info_bar.connect(self.push_info_bar.emit)
self.task.accomplish.connect(lambda: self.accomplish.emit([]))
self.task.run()
else:
self.member_dict = self.search_member()
self.task_dict = [
[value, "等待"]
for _, value in self.info["Queue"].items()
if value != "禁用"
]
self.create_task_list.emit(self.task_dict)
for i in range(len(self.task_dict)):
if self.isInterruptionRequested():
break
self.task_dict[i][1] = "运行"
self.update_task_list.emit(self.task_dict)
if self.task_dict[i][0] in Config.running_list:
self.task_dict[i][1] = "跳过"
self.update_task_list.emit(self.task_dict)
logger.info(f"跳过任务:{self.task_dict[i][0]}")
self.push_info_bar.emit(
"info", "跳过任务", self.task_dict[i][0], 3000
)
continue
Config.running_list.append(self.task_dict[i][0])
logger.info(f"任务开始:{self.task_dict[i][0]}")
self.push_info_bar.emit("info", "任务开始", self.task_dict[i][0], 3000)
if self.member_dict[self.task_dict[i][0]][0] == "Maa":
self.task = MaaManager(
self.mode[0:4],
self.member_dict[self.task_dict[i][0]][1],
)
self.task.question.connect(self.question.emit)
self.question_response.disconnect()
self.question_response.connect(self.task.question_response.emit)
self.task.push_info_bar.connect(self.push_info_bar.emit)
self.task.create_user_list.connect(self.create_user_list.emit)
self.task.update_user_list.connect(self.update_user_list.emit)
self.task.update_log_text.connect(self.update_log_text.emit)
self.task.update_user_info.connect(
lambda modes, uids, days, lasts, notes, numbs: self.update_user_info.emit(
self.member_dict[self.task_dict[i][0]][1],
modes,
uids,
days,
lasts,
notes,
numbs,
)
)
self.task.accomplish.connect(
lambda log: self.task_accomplish(self.task_dict[i][0], log)
)
self.task.run()
Config.running_list.remove(self.task_dict[i][0])
self.task_dict[i][1] = "完成"
logger.info(f"任务完成:{self.task_dict[i][0]}")
self.push_info_bar.emit("info", "任务完成", self.task_dict[i][0], 3000)
self.accomplish.emit(self.logs)
def search_member(self) -> dict:
"""搜索所有脚本实例及其路径"""
member_dict = {}
if (Config.app_path / "config/MaaConfig").exists():
for subdir in (Config.app_path / "config/MaaConfig").iterdir():
if subdir.is_dir():
member_dict[subdir.name] = ["Maa", subdir]
return member_dict
def task_accomplish(self, name: str, log: dict):
"""保存保存任务结果"""
self.logs.append([name, log])
self.task.deleteLater()
class _TaskManager(QObject):
"""业务调度器"""
create_gui = Signal(Task)
connect_gui = Signal(Task)
push_info_bar = Signal(str, str, str, int)
def __init__(self):
super(_TaskManager, self).__init__()
self.task_dict: Dict[str, Task] = {}
def add_task(
self, mode: str, name: str, info: Dict[str, Dict[str, Union[str, int, bool]]]
):
"""添加任务"""
if name in Config.running_list or name in self.task_dict:
logger.warning(f"任务已存在:{name}")
MainInfoBar.push_info_bar("warning", "任务已存在", name, 5000)
return None
logger.info(f"任务开始:{name}")
MainInfoBar.push_info_bar("info", "任务开始", name, 3000)
Config.running_list.append(name)
self.task_dict[name] = Task(mode, name, info)
self.task_dict[name].question.connect(
lambda title, content: self.push_dialog(name, title, content)
)
self.task_dict[name].push_info_bar.connect(MainInfoBar.push_info_bar)
self.task_dict[name].update_user_info.connect(Config.change_user_info)
self.task_dict[name].accomplish.connect(
lambda logs: self.remove_task(mode, name, logs)
)
if "新调度台" in mode:
self.create_gui.emit(self.task_dict[name])
elif "主调度台" in mode:
self.connect_gui.emit(self.task_dict[name])
self.task_dict[name].start()
def stop_task(self, name: str):
"""中止任务"""
logger.info(f"中止任务:{name}")
MainInfoBar.push_info_bar("info", "中止任务", name, 3000)
if name == "ALL":
for name in self.task_dict:
self.task_dict[name].task.requestInterruption()
self.task_dict[name].requestInterruption()
self.task_dict[name].quit()
self.task_dict[name].wait()
elif name in self.task_dict:
self.task_dict[name].task.requestInterruption()
self.task_dict[name].requestInterruption()
self.task_dict[name].quit()
self.task_dict[name].wait()
def remove_task(self, mode: str, name: str, logs: str):
"""任务结束后的处理"""
logger.info(f"任务结束:{name}")
MainInfoBar.push_info_bar("info", "任务结束", name, 3000)
self.task_dict[name].deleteLater()
if len(logs) > 0:
time = logs[0][1]["Time"]
history = ""
for log in logs:
Config.save_history(log[0], log[1])
history += (
f"任务名称:{log[0]}{log[1]["History"].replace("\n","\n ")}\n"
)
Config.save_history(name, {"Time": time, "History": history})
else:
Config.save_history(
name,
{
"Time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"History": "没有任务被执行",
},
)
self.task_dict.pop(name)
Config.running_list.remove(name)
if "调度队列" in name and "人工排查" not in mode:
with (Config.app_path / f"config/QueueConfig/{name}.json").open(
"r", encoding="utf-8"
) as f:
info = json.load(f)
System.set_power(info["QueueSet"]["AfterAccomplish"])
def push_dialog(self, name: str, title: str, content: str):
"""推送对话框"""
choice = Dialog(title, content, None)
choice.yesButton.setText("")
choice.cancelButton.setText("")
self.task_dict[name].question_response.emit(bool(choice.exec_()))
TaskManager = _TaskManager()

125
app/core/timer.py Normal file
View File

@@ -0,0 +1,125 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA主业务定时器
v4.2
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import QWidget
from PySide6.QtCore import QTimer
import json
from datetime import datetime
import pyautogui
from .config import Config
from .task_manager import TaskManager
from app.services import System
class _MainTimer(QWidget):
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.start(1000)
def timed_start(self):
"""定时启动代理任务"""
# 获取定时列表
queue_list = self.search_queue()
for i in queue_list:
name, info = i
if not info["QueueSet"]["Enabled"]:
continue
history = Config.get_history(name)
time_set = [
info["Time"][f"TimeSet_{_}"]
for _ in range(10)
if info["Time"][f"TimeEnabled_{_}"]
]
# 按时间调起代理任务
curtime = datetime.now().strftime("%Y-%m-%d %H:%M")
if (
curtime[11:16] in time_set
and curtime != history["Time"][:16]
and name not in Config.running_list
):
logger.info(f"定时任务:{name}")
TaskManager.add_task("自动代理_新调度台", name, info)
def set_silence(self):
"""设置静默模式"""
if (
Config.global_config.get(Config.global_config.function_IfSilence)
and Config.global_config.get(Config.global_config.function_BossKey) != ""
):
windows = System.get_window_info()
if any(
str(emulator_path) in window
for window in windows
for emulator_path in Config.silence_list
):
try:
pyautogui.hotkey(
*[
_.strip().lower()
for _ in Config.global_config.get(
Config.global_config.function_BossKey
).split("+")
]
)
except pyautogui.FailSafeException as e:
if not self.if_FailSafeException:
logger.warning(f"FailSafeException: {e}")
self.if_FailSafeException = True
def search_queue(self) -> list:
"""搜索所有调度队列实例"""
queue_list = []
if (Config.app_path / "config/QueueConfig").exists():
for json_file in (Config.app_path / "config/QueueConfig").glob("*.json"):
with json_file.open("r", encoding="utf-8") as f:
info = json.load(f)
queue_list.append([json_file.stem, info])
return queue_list
MainTimer = _MainTimer()

1258
app/models/MAA.py Normal file

File diff suppressed because it is too large Load Diff

34
app/models/__init__.py Normal file
View File

@@ -0,0 +1,34 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA模组包
v4.2
作者DLmaster_361
"""
__version__ = "4.2.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .MAA import MaaManager
__all__ = ["MaaManager"]

36
app/services/__init__.py Normal file
View File

@@ -0,0 +1,36 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA服务包
v4.2
作者DLmaster_361
"""
__version__ = "4.2.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .notification import Notify
from .security import Crypto
from .system import System
__all__ = ["Notify", "Crypto", "System"]

View File

@@ -0,0 +1,247 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA通知服务
v4.2
作者DLmaster_361
"""
from PySide6.QtWidgets import QWidget
from PySide6.QtCore import Signal
import requests
from loguru import logger
from plyer import notification
import re
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from email.utils import formataddr
from serverchan_sdk import sc_send
from app.core import Config
from app.services.security import Crypto
class Notification(QWidget):
push_info_bar = Signal(str, str, str, int)
def __init__(self, parent=None):
super().__init__(parent)
def push_plyer(self, title, message, ticker, t):
"""推送系统通知"""
if Config.global_config.get(Config.global_config.notify_IfPushPlyer):
notification.notify(
title=title,
message=message,
app_name="AUTO_MAA",
app_icon=str(Config.app_path / "resources/icons/AUTO_MAA.ico"),
timeout=t,
ticker=ticker,
toast=True,
)
return True
def send_mail(self, mode, title, content) -> None:
"""推送邮件通知"""
if Config.global_config.get(Config.global_config.notify_IfSendMail):
if (
Config.global_config.get(Config.global_config.notify_SMTPServerAddress)
== ""
or Config.global_config.get(
Config.global_config.notify_AuthorizationCode
)
== ""
or not bool(
re.match(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
Config.global_config.get(
Config.global_config.notify_FromAddress
),
)
)
or not bool(
re.match(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
Config.global_config.get(Config.global_config.notify_ToAddress),
)
)
):
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.global_config.get(
Config.global_config.notify_FromAddress
),
)
) # 发件人显示的名字
message["To"] = formataddr(
(
Header("AUTO_MAA用户", "utf-8").encode(),
Config.global_config.get(Config.global_config.notify_ToAddress),
)
) # 收件人显示的名字
message["Subject"] = Header(title, "utf-8")
if mode == "网页":
message.attach(MIMEText(content, "html", "utf-8"))
smtpObj = smtplib.SMTP_SSL(
Config.global_config.get(
Config.global_config.notify_SMTPServerAddress
),
465,
)
smtpObj.login(
Config.global_config.get(Config.global_config.notify_FromAddress),
Crypto.win_decryptor(
Config.global_config.get(
Config.global_config.notify_AuthorizationCode
)
),
)
smtpObj.sendmail(
Config.global_config.get(Config.global_config.notify_FromAddress),
Config.global_config.get(Config.global_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酱推送通知"""
if Config.global_config.get(Config.global_config.notify_IfServerChan):
send_key = Config.global_config.get(
Config.global_config.notify_ServerChanKey
)
option = {}
is_valid = lambda s: s == "" or (
s == "|".join(s.split("|")) and (s.count("|") == 0 or all(s.split("|")))
)
"""
is_valid => True, 如果启用的话需要正确设置Tag和Channel。
允许空的Tag和Channel即不启用但不允许例如a||b|a|ba|b|||||
"""
send_tag = Config.global_config.get(
Config.global_config.notify_ServerChanTag
)
send_channel = Config.global_config.get(
Config.global_config.notify_ServerChanChannel
)
if is_valid(send_tag):
option["tags"] = send_tag
else:
option["tags"] = ""
logger.warning("请正确设置Auto_MAA中ServerChan的Tag。")
self.push_info_bar.emit(
"warning",
"Server酱通知推送异常",
"请正确设置Auto_MAA中ServerChan的Tag。",
-1,
)
if is_valid(send_channel):
option["channel"] = send_channel
else:
option["channel"] = ""
logger.warning("请正确设置Auto_MAA中ServerChan的Channel。")
self.push_info_bar.emit(
"warning",
"Server酱通知推送异常",
"请正确设置Auto_MAA中ServerChan的Channel。",
-1,
)
response = sc_send(send_key, title, content, option)
if response["code"] == 0:
logger.info("Server酱推送通知成功")
return True
else:
logger.info("Server酱推送通知失败")
logger.error(response)
self.push_info_bar.emit(
"error",
"Server酱通知推送失败",
f'使用Server酱推送通知时出错\n{response["data"]['error']}',
-1,
)
return f'使用Server酱推送通知时出错\n{response["data"]['error']}'
def CompanyWebHookBotPush(self, title, content):
"""使用企业微信群机器人推送通知"""
if Config.global_config.get(Config.global_config.notify_IfCompanyWebHookBot):
content = f"{title}\n{content}"
data = {"msgtype": "text", "text": {"content": content}}
response = requests.post(
url=Config.global_config.get(
Config.global_config.notify_CompanyWebHookBotUrl
),
json=data,
)
if response.json()["errcode"] == 0:
logger.info("企业微信群机器人推送通知成功")
return True
else:
logger.info("企业微信群机器人推送通知失败")
logger.error(response.json())
self.push_info_bar.emit(
"error",
"企业微信群机器人通知推送失败",
f'使用企业微信群机器人推送通知时出错:\n{response.json()["errmsg"]}',
-1,
)
return (
f'使用企业微信群机器人推送通知时出错:\n{response.json()["errmsg"]}'
)
Notify = Notification()

230
app/services/security.py Normal file
View File

@@ -0,0 +1,230 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA安全服务
v4.2
作者DLmaster_361
"""
from loguru import logger
import sqlite3
import hashlib
import random
import secrets
import base64
import win32crypt
from pathlib import Path
from Crypto.Cipher import AES
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Util.Padding import pad, unpad
from typing import List, Dict, Union
from app.core import Config
class CryptoHandler:
def get_PASSWORD(self, PASSWORD: str) -> None:
"""配置管理密钥"""
# 生成目录
Config.key_path.mkdir(parents=True, exist_ok=True)
# 生成RSA密钥对
key = RSA.generate(2048)
public_key_local = key.publickey()
private_key = key
# 保存RSA公钥
(Config.app_path / "data/key/public_key.pem").write_bytes(
public_key_local.exportKey()
)
# 生成密钥转换与校验随机盐
PASSWORD_salt = secrets.token_hex(random.randint(32, 1024))
(Config.app_path / "data/key/PASSWORDsalt.txt").write_text(
PASSWORD_salt,
encoding="utf-8",
)
verify_salt = secrets.token_hex(random.randint(32, 1024))
(Config.app_path / "data/key/verifysalt.txt").write_text(
verify_salt,
encoding="utf-8",
)
# 将管理密钥转化为AES-256密钥
AES_password = hashlib.sha256(
(PASSWORD + PASSWORD_salt).encode("utf-8")
).digest()
# 生成AES-256密钥校验哈希值并保存
AES_password_verify = hashlib.sha256(
AES_password + verify_salt.encode("utf-8")
).digest()
(Config.app_path / "data/key/AES_password_verify.bin").write_bytes(
AES_password_verify
)
# AES-256加密RSA私钥并保存密文
AES_key = AES.new(AES_password, AES.MODE_ECB)
private_key_local = AES_key.encrypt(pad(private_key.exportKey(), 32))
(Config.app_path / "data/key/private_key.bin").write_bytes(private_key_local)
def AUTO_encryptor(self, note: str) -> bytes:
"""使用AUTO_MAA的算法加密数据"""
# 读取RSA公钥
public_key_local = RSA.import_key(
(Config.app_path / "data/key/public_key.pem").read_bytes()
)
# 使用RSA公钥对数据进行加密
cipher = PKCS1_OAEP.new(public_key_local)
encrypted = cipher.encrypt(note.encode("utf-8"))
return encrypted
def AUTO_decryptor(self, note: bytes, PASSWORD: str) -> str:
"""使用AUTO_MAA的算法解密数据"""
# 读入RSA私钥密文、盐与校验哈希值
private_key_local = (
(Config.app_path / "data/key/private_key.bin").read_bytes().strip()
)
PASSWORD_salt = (
(Config.app_path / "data/key/PASSWORDsalt.txt")
.read_text(encoding="utf-8")
.strip()
)
verify_salt = (
(Config.app_path / "data/key/verifysalt.txt")
.read_text(encoding="utf-8")
.strip()
)
AES_password_verify = (
(Config.app_path / "data/key/AES_password_verify.bin").read_bytes().strip()
)
# 将管理密钥转化为AES-256密钥并验证
AES_password = hashlib.sha256(
(PASSWORD + PASSWORD_salt).encode("utf-8")
).digest()
AES_password_SHA = hashlib.sha256(
AES_password + verify_salt.encode("utf-8")
).digest()
if AES_password_SHA != AES_password_verify:
return "管理密钥错误"
else:
# AES解密RSA私钥
AES_key = AES.new(AES_password, AES.MODE_ECB)
private_key_pem = unpad(AES_key.decrypt(private_key_local), 32)
private_key = RSA.import_key(private_key_pem)
# 使用RSA私钥解密数据
decrypter = PKCS1_OAEP.new(private_key)
note = decrypter.decrypt(note)
return note.decode("utf-8")
def change_PASSWORD(self, PASSWORD_old: str, PASSWORD_new: str) -> None:
"""修改管理密钥"""
member_list = self.search_member()
for user_data in member_list:
# 读取用户数据
db = sqlite3.connect(user_data["Path"])
cur = db.cursor()
cur.execute("SELECT * FROM adminx WHERE True")
data = cur.fetchall()
# 使用旧管理密钥解密
user_data["Password"] = []
for i in range(len(data)):
user_data["Password"].append(
self.AUTO_decryptor(data[i][12], PASSWORD_old)
)
cur.close()
db.close()
self.get_PASSWORD(PASSWORD_new)
for user_data in member_list:
# 读取用户数据
db = sqlite3.connect(user_data["Path"])
cur = db.cursor()
cur.execute("SELECT * FROM adminx WHERE True")
data = cur.fetchall()
# 使用新管理密钥重新加密
for i in range(len(data)):
cur.execute(
"UPDATE adminx SET password = ? WHERE mode = ? AND uid = ?",
(
self.AUTO_encryptor(user_data["Password"][i]),
data[i][15],
data[i][16],
),
)
db.commit()
user_data["Password"][i] = None
del user_data["Password"]
cur.close()
db.close()
def win_encryptor(
self, note: str, description: str = None, entropy: bytes = None
) -> str:
"""使用Windows DPAPI加密数据"""
encrypted = win32crypt.CryptProtectData(
note.encode("utf-8"), description, entropy, None, None, 0
)
return base64.b64encode(encrypted).decode("utf-8")
def win_decryptor(self, note: str, entropy: bytes = None) -> str:
"""使用Windows DPAPI解密数据"""
if note == "":
return ""
decrypted = win32crypt.CryptUnprotectData(
base64.b64decode(note), entropy, None, None, 0
)
return decrypted[1].decode("utf-8")
def search_member(self) -> List[Dict[str, Union[Path, list]]]:
"""搜索所有脚本实例及其用户数据库路径"""
member_list = []
if (Config.app_path / "config/MaaConfig").exists():
for subdir in (Config.app_path / "config/MaaConfig").iterdir():
if subdir.is_dir():
member_list.append({"Path": subdir / "user_data.db"})
return member_list
def check_PASSWORD(self, PASSWORD: str) -> bool:
"""验证管理密钥"""
return bool(
self.AUTO_decryptor(self.AUTO_encryptor(""), PASSWORD) != "管理密钥错误"
)
Crypto = CryptoHandler()

205
app/services/system.py Normal file
View File

@@ -0,0 +1,205 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA系统服务
v4.2
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import QWidget
import sys
import ctypes
import win32gui
import win32process
import winreg
import psutil
import subprocess
from pathlib import Path
from app.core import Config
class _SystemHandler:
ES_CONTINUOUS = 0x80000000
ES_SYSTEM_REQUIRED = 0x00000001
def __init__(self, main_window: QWidget = None):
self.main_window = main_window
self.set_Sleep()
self.set_SelfStart()
def set_Sleep(self) -> None:
"""同步系统休眠状态"""
if Config.global_config.get(Config.global_config.function_IfAllowSleep):
# 设置系统电源状态
ctypes.windll.kernel32.SetThreadExecutionState(
self.ES_CONTINUOUS | self.ES_SYSTEM_REQUIRED
)
else:
# 恢复系统电源状态
ctypes.windll.kernel32.SetThreadExecutionState(self.ES_CONTINUOUS)
def set_SelfStart(self) -> None:
"""同步开机自启"""
if (
Config.global_config.get(Config.global_config.start_IfSelfStart)
and not self.is_startup()
):
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Run",
winreg.KEY_SET_VALUE,
winreg.KEY_ALL_ACCESS | winreg.KEY_WRITE | winreg.KEY_CREATE_SUB_KEY,
)
winreg.SetValueEx(key, "AUTO_MAA", 0, winreg.REG_SZ, Config.app_path_sys)
winreg.CloseKey(key)
elif (
not Config.global_config.get(Config.global_config.start_IfSelfStart)
and self.is_startup()
):
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Run",
winreg.KEY_SET_VALUE,
winreg.KEY_ALL_ACCESS | winreg.KEY_WRITE | winreg.KEY_CREATE_SUB_KEY,
)
winreg.DeleteValue(key, "AUTO_MAA")
winreg.CloseKey(key)
def set_power(self, mode) -> None:
if sys.platform.startswith("win"):
if mode == "None":
logger.info("不执行系统电源操作")
elif mode == "Shutdown":
logger.info("执行关机操作")
subprocess.run(["shutdown", "/s", "/t", "0"])
elif mode == "Hibernate":
logger.info("执行休眠操作")
subprocess.run(["shutdown", "/h"])
elif mode == "Sleep":
logger.info("执行睡眠操作")
subprocess.run(
["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"]
)
elif mode == "KillSelf":
self.main_window.close()
elif sys.platform.startswith("linux"):
if mode == "None":
logger.info("不执行系统电源操作")
elif mode == "Shutdown":
logger.info("执行关机操作")
subprocess.run(["shutdown", "-h", "now"])
elif mode == "Hibernate":
logger.info("执行休眠操作")
subprocess.run(["systemctl", "hibernate"])
elif mode == "Sleep":
logger.info("执行睡眠操作")
subprocess.run(["systemctl", "suspend"])
elif mode == "KillSelf":
self.main_window.close()
def is_startup(self) -> bool:
"""判断程序是否已经开机自启"""
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Run",
0,
winreg.KEY_READ,
)
try:
value, _ = winreg.QueryValueEx(key, "AUTO_MAA")
winreg.CloseKey(key)
return True
except FileNotFoundError:
winreg.CloseKey(key)
return False
def get_window_info(self) -> list:
"""获取当前窗口信息"""
def callback(hwnd, window_info):
if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd):
_, pid = win32process.GetWindowThreadProcessId(hwnd)
process = psutil.Process(pid)
window_info.append((win32gui.GetWindowText(hwnd), process.exe()))
return True
window_info = []
win32gui.EnumWindows(callback, window_info)
return window_info
def kill_process(self, path: Path) -> None:
"""根据路径中止进程"""
for pid in self.search_pids(path):
killprocess = subprocess.Popen(
f"taskkill /F /T /PID {pid}",
shell=True,
creationflags=subprocess.CREATE_NO_WINDOW,
)
killprocess.wait()
def search_pids(self, path: Path) -> list:
"""根据路径查找进程PID"""
pids = []
for proc in psutil.process_iter(["pid", "exe"]):
try:
if proc.info["exe"] and proc.info["exe"].lower() == str(path).lower():
pids.append(proc.info["pid"])
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
# 进程可能在此期间已结束或无法访问,忽略这些异常
pass
return pids
System = _SystemHandler()

534
app/ui/Widget.py Normal file
View File

@@ -0,0 +1,534 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA组件
v4.2
作者DLmaster_361
"""
from PySide6.QtWidgets import QWidget, QHBoxLayout
from PySide6.QtCore import Qt, QTime, QEvent
from PySide6.QtGui import QIcon, QPixmap, QPainter, QPainterPath
from qfluentwidgets import (
LineEdit,
PasswordLineEdit,
MessageBoxBase,
SubtitleLabel,
SettingCard,
SpinBox,
FluentIconBase,
Signal,
ComboBox,
CheckBox,
IconWidget,
FluentIcon,
CardWidget,
BodyLabel,
qconfig,
ConfigItem,
TimeEdit,
OptionsConfigItem,
TeachingTip,
TransparentToolButton,
TeachingTipTailPosition,
)
from qfluentwidgets.common.overload import singledispatchmethod
import os
from typing import Optional, Union, List
from app.services import Crypto
class LineEditMessageBox(MessageBoxBase):
"""输入对话框"""
def __init__(self, parent, title: str, content: str, mode: str):
super().__init__(parent)
self.title = SubtitleLabel(title)
if mode == "明文":
self.input = LineEdit()
self.input.setClearButtonEnabled(True)
elif mode == "密码":
self.input = PasswordLineEdit()
self.input.setPlaceholderText(content)
# 将组件添加到布局中
self.viewLayout.addWidget(self.title)
self.viewLayout.addWidget(self.input)
class ComboBoxMessageBox(MessageBoxBase):
"""选择对话框"""
def __init__(self, parent, title: str, content: List[str], list: List[List[str]]):
super().__init__(parent)
self.title = SubtitleLabel(title)
Widget = QWidget()
Layout = QHBoxLayout(Widget)
self.input: List[ComboBox] = []
for i in range(len(content)):
self.input.append(ComboBox())
self.input[i].addItems(list[i])
self.input[i].setCurrentIndex(-1)
self.input[i].setPlaceholderText(content[i])
Layout.addWidget(self.input[i])
# 将组件添加到布局中
self.viewLayout.addWidget(self.title)
self.viewLayout.addWidget(Widget)
class LineEditSettingCard(SettingCard):
"""Setting card with LineEdit"""
textChanged = Signal(str)
def __init__(
self,
text,
icon: Union[str, QIcon, FluentIconBase],
title,
content=None,
configItem: ConfigItem = None,
parent=None,
):
super().__init__(icon, title, content, parent)
self.configItem = configItem
self.LineEdit = LineEdit(self)
self.LineEdit.setMinimumWidth(250)
self.LineEdit.setPlaceholderText(text)
if configItem:
self.setValue(qconfig.get(configItem))
configItem.valueChanged.connect(self.setValue)
self.hBoxLayout.addWidget(self.LineEdit, 0, Qt.AlignRight)
self.hBoxLayout.addSpacing(16)
self.LineEdit.textChanged.connect(self.__textChanged)
def __textChanged(self, content: str):
self.setValue(content)
self.textChanged.emit(content)
def setValue(self, content: str):
if self.configItem:
qconfig.set(self.configItem, content)
self.LineEdit.setText(content)
class PasswordLineEditSettingCard(SettingCard):
"""Setting card with PasswordLineEdit"""
textChanged = Signal(str)
def __init__(
self,
text,
icon: Union[str, QIcon, FluentIconBase],
title,
content=None,
configItem: ConfigItem = None,
parent=None,
):
super().__init__(icon, title, content, parent)
self.configItem = configItem
self.LineEdit = PasswordLineEdit(self)
self.LineEdit.setMinimumWidth(250)
self.LineEdit.setPlaceholderText(text)
if configItem:
self.setValue(qconfig.get(configItem))
configItem.valueChanged.connect(self.setValue)
self.hBoxLayout.addWidget(self.LineEdit, 0, Qt.AlignRight)
self.hBoxLayout.addSpacing(16)
self.LineEdit.textChanged.connect(self.__textChanged)
def __textChanged(self, content: str):
self.setValue(Crypto.win_encryptor(content))
self.textChanged.emit(content)
def setValue(self, content: str):
if self.configItem:
qconfig.set(self.configItem, content)
self.LineEdit.setText(Crypto.win_decryptor(content))
class SpinBoxSettingCard(SettingCard):
"""Setting card with SpinBox"""
textChanged = Signal(int)
def __init__(
self,
range: tuple[int, int],
icon: Union[str, QIcon, FluentIconBase],
title,
content=None,
configItem: ConfigItem = None,
parent=None,
):
super().__init__(icon, title, content, parent)
self.configItem = configItem
self.SpinBox = SpinBox(self)
self.SpinBox.setRange(range[0], range[1])
self.SpinBox.setMinimumWidth(150)
if configItem:
self.setValue(qconfig.get(configItem))
configItem.valueChanged.connect(self.setValue)
self.hBoxLayout.addWidget(self.SpinBox, 0, Qt.AlignRight)
self.hBoxLayout.addSpacing(16)
self.SpinBox.valueChanged.connect(self.__valueChanged)
def __valueChanged(self, value: int):
self.setValue(value)
self.textChanged.emit(value)
def setValue(self, value: int):
if self.configItem:
qconfig.set(self.configItem, value)
self.SpinBox.setValue(value)
class NoOptionComboBoxSettingCard(SettingCard):
def __init__(
self,
configItem: OptionsConfigItem,
icon: Union[str, QIcon, FluentIconBase],
title,
content=None,
value=None,
texts=None,
parent=None,
):
super().__init__(icon, title, content, parent)
self.configItem = configItem
self.comboBox = ComboBox(self)
self.comboBox.setMinimumWidth(250)
self.hBoxLayout.addWidget(self.comboBox, 0, Qt.AlignRight)
self.hBoxLayout.addSpacing(16)
self.optionToText = {o: t for o, t in zip(value, texts)}
for text, option in zip(texts, value):
self.comboBox.addItem(text, userData=option)
self.comboBox.setCurrentText(self.optionToText[qconfig.get(configItem)])
self.comboBox.currentIndexChanged.connect(self._onCurrentIndexChanged)
configItem.valueChanged.connect(self.setValue)
def _onCurrentIndexChanged(self, index: int):
qconfig.set(self.configItem, self.comboBox.itemData(index))
def setValue(self, value):
if value not in self.optionToText:
return
self.comboBox.setCurrentText(self.optionToText[value])
qconfig.set(self.configItem, value)
class TimeEditSettingCard(SettingCard):
enabledChanged = Signal(bool)
timeChanged = Signal(str)
def __init__(
self,
icon: Union[str, QIcon, FluentIconBase],
title,
content=None,
configItem_bool: ConfigItem = None,
configItem_time: ConfigItem = None,
parent=None,
):
super().__init__(icon, title, content, parent)
self.configItem_bool = configItem_bool
self.configItem_time = configItem_time
self.CheckBox = CheckBox(self)
self.CheckBox.setTristate(False)
self.TimeEdit = TimeEdit(self)
self.TimeEdit.setDisplayFormat("HH:mm")
self.TimeEdit.setMinimumWidth(150)
if configItem_bool:
self.setValue_bool(qconfig.get(configItem_bool))
configItem_bool.valueChanged.connect(self.setValue_bool)
if configItem_time:
self.setValue_time(qconfig.get(configItem_time))
configItem_time.valueChanged.connect(self.setValue_time)
self.hBoxLayout.addWidget(self.CheckBox, 0, Qt.AlignRight)
self.hBoxLayout.addWidget(self.TimeEdit, 0, Qt.AlignRight)
self.hBoxLayout.addSpacing(16)
self.CheckBox.stateChanged.connect(self.__enableChanged)
self.TimeEdit.timeChanged.connect(self.__timeChanged)
def __timeChanged(self, value: QTime):
self.setValue_time(value.toString("HH:mm"))
self.timeChanged.emit(value.toString("HH:mm"))
def __enableChanged(self, value: int):
if value == 0:
self.setValue_bool(False)
self.enabledChanged.emit(False)
else:
self.setValue_bool(True)
self.enabledChanged.emit(True)
def setValue_bool(self, value: bool):
if self.configItem_bool:
qconfig.set(self.configItem_bool, value)
self.CheckBox.setChecked(value)
def setValue_time(self, value: str):
if self.configItem_time:
qconfig.set(self.configItem_time, value)
self.TimeEdit.setTime(QTime.fromString(value, "HH:mm"))
class StatefulItemCard(CardWidget):
def __init__(self, item: list, parent=None):
super().__init__(parent)
self.Layout = QHBoxLayout(self)
self.Label = BodyLabel(item[0], self)
self.icon = IconWidget(FluentIcon.MORE, self)
self.icon.setFixedSize(16, 16)
self.update_status(item[1])
self.Layout.addWidget(self.icon)
self.Layout.addWidget(self.Label)
self.Layout.addStretch(1)
def update_status(self, status: str):
if status == "完成":
self.icon.setIcon(FluentIcon.ACCEPT)
self.Label.setTextColor("#0eb840", "#0eb840")
elif status == "等待":
self.icon.setIcon(FluentIcon.MORE)
self.Label.setTextColor("#161823", "#e3f9fd")
elif status == "运行":
self.icon.setIcon(FluentIcon.PLAY)
self.Label.setTextColor("#177cb0", "#70f3ff")
elif status == "跳过":
self.icon.setIcon(FluentIcon.REMOVE)
self.Label.setTextColor("#75878a", "#7397ab")
elif status == "异常":
self.icon.setIcon(FluentIcon.CLOSE)
self.Label.setTextColor("#ff2121", "#ff2121")
class QuantifiedItemCard(CardWidget):
def __init__(self, item: list, parent=None):
super().__init__(parent)
self.Layout = QHBoxLayout(self)
self.Name = BodyLabel(item[0], self)
self.Numb = BodyLabel(str(item[1]), self)
self.Layout.addWidget(self.Name)
self.Layout.addStretch(1)
self.Layout.addWidget(self.Numb)
class IconButton(TransparentToolButton):
"""包含下拉框的自定义设置卡片类。"""
@singledispatchmethod
def __init__(self, parent: QWidget = None):
TransparentToolButton.__init__(self, parent)
self._tooltip: Optional[TeachingTip] = None
@__init__.register
def _(self, icon: Union[str, QIcon, FluentIconBase], parent: QWidget = None):
self.__init__(parent)
self.setIcon(icon)
@__init__.register
def _(
self,
icon: Union[str, QIcon, FluentIconBase],
isTooltip: bool,
tip_title: str,
tip_content: str,
parent: QWidget = None,
):
self.__init__(parent)
self.setIcon(icon)
# 处理工具提示
if isTooltip:
self.installEventFilter(self)
self.tip_title: str = tip_title
self.tip_content: str = tip_content
def eventFilter(self, obj, event: QEvent) -> bool:
"""处理鼠标事件。"""
if event.type() == QEvent.Type.Enter:
self._show_tooltip()
elif event.type() == QEvent.Type.Leave:
self._hide_tooltip()
return super().eventFilter(obj, event)
def _show_tooltip(self) -> None:
"""显示工具提示。"""
self._tooltip = TeachingTip.create(
target=self,
title=self.tip_title,
content=self.tip_content,
tailPosition=TeachingTipTailPosition.RIGHT,
isClosable=False,
duration=-1,
parent=self,
)
# 设置偏移
if self._tooltip:
tooltip_pos = self.mapToGlobal(self.rect().topRight())
tooltip_pos.setX(
tooltip_pos.x() - self._tooltip.size().width() - 40
) # 水平偏移
tooltip_pos.setY(
tooltip_pos.y() - self._tooltip.size().height() / 2 + 35
) # 垂直偏移
self._tooltip.move(tooltip_pos)
def _hide_tooltip(self) -> None:
"""隐藏工具提示。"""
if self._tooltip:
self._tooltip.close()
self._tooltip = None
def __hash__(self):
return id(self)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self is other
class Banner(QWidget):
"""展示带有圆角的固定大小横幅小部件"""
def __init__(self, image_path: str = None, parent=None):
QWidget.__init__(self, parent)
self.image_path = None
self.banner_image = None
self.scaled_image = None
if image_path:
self.set_banner_image(image_path)
def set_banner_image(self, image_path: str):
"""设置横幅图片"""
self.image_path = image_path
self.banner_image = self.load_banner_image(image_path)
self.update_scaled_image()
def load_banner_image(self, image_path: str) -> QPixmap:
"""加载横幅图片,或创建渐变备用图片"""
if os.path.isfile(image_path):
return QPixmap(image_path)
return self._create_fallback_image()
def _create_fallback_image(self):
"""创建渐变备用图片"""
fallback_image = QPixmap(2560, 1280) # 使用原始图片的大小
fallback_image.fill(Qt.GlobalColor.gray)
return fallback_image
def update_scaled_image(self):
"""按高度缩放图片,宽度保持比例,超出裁剪"""
if self.banner_image:
self.scaled_image = self.banner_image.scaled(
self.size(),
Qt.AspectRatioMode.KeepAspectRatioByExpanding,
Qt.TransformationMode.SmoothTransformation,
)
self.update()
def paintEvent(self, event):
"""重载 paintEvent 以绘制缩放后的图片"""
if self.scaled_image:
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
# 创建圆角路径
path = QPainterPath()
path.addRoundedRect(self.rect(), 20, 20)
painter.setClipPath(path)
# 计算绘制位置,使图片居中
x = (self.width() - self.scaled_image.width()) // 2
y = (self.height() - self.scaled_image.height()) // 2
# 绘制缩放后的图片
painter.drawPixmap(x, y, self.scaled_image)
def resizeEvent(self, event):
"""重载 resizeEvent 以更新缩放后的图片"""
self.update_scaled_image()
QWidget.resizeEvent(self, event)
def set_percentage_size(self, width_percentage, height_percentage):
"""设置 Banner 的大小为窗口大小的百分比"""
parent = self.parentWidget()
if parent:
new_width = int(parent.width() * width_percentage)
new_height = int(parent.height() * height_percentage)
self.setFixedSize(new_width, new_height)
self.update_scaled_image()

34
app/ui/__init__.py Normal file
View File

@@ -0,0 +1,34 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA图形化界面包
v4.2
作者DLmaster_361
"""
__version__ = "4.2.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .main_window import AUTO_MAA
__all__ = ["AUTO_MAA"]

420
app/ui/dispatch_center.py Normal file
View File

@@ -0,0 +1,420 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA调度中枢界面
v4.2
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QStackedWidget,
QHBoxLayout,
)
from qfluentwidgets import (
CardWidget,
Pivot,
ScrollArea,
FluentIcon,
HeaderCardWidget,
FluentIcon,
TextBrowser,
ComboBox,
SubtitleLabel,
PushButton,
)
from PySide6.QtCore import Qt
from PySide6.QtGui import QTextCursor
from typing import List, Dict
import json
from app.core import Config, TaskManager, Task, MainInfoBar
from .Widget import StatefulItemCard
class DispatchCenter(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("调度中枢")
self.pivot = Pivot(self)
self.stackedWidget = QStackedWidget(self)
self.Layout = QVBoxLayout(self)
self.script_list: Dict[str, DispatchBox] = {}
dispatch_box = DispatchBox("主调度台", self)
self.script_list["主调度台"] = dispatch_box
self.stackedWidget.addWidget(self.script_list["主调度台"])
self.pivot.addItem(
routeKey="主调度台",
text="主调度台",
onClick=self.update_top_bar,
icon=FluentIcon.CAFE,
)
self.Layout.addWidget(self.pivot, 0, Qt.AlignHCenter)
self.Layout.addWidget(self.stackedWidget)
self.Layout.setContentsMargins(0, 0, 0, 0)
self.pivot.currentItemChanged.connect(
lambda index: self.stackedWidget.setCurrentWidget(self.script_list[index])
)
def add_board(self, task: Task) -> None:
"""添加一个调度台界面"""
dispatch_box = DispatchBox(task.name, self)
dispatch_box.top_bar.button.clicked.connect(
lambda: TaskManager.stop_task(task.name)
)
task.create_task_list.connect(dispatch_box.info.task.create_task)
task.create_user_list.connect(dispatch_box.info.user.create_user)
task.update_task_list.connect(dispatch_box.info.task.update_task)
task.update_user_list.connect(dispatch_box.info.user.update_user)
task.update_log_text.connect(dispatch_box.info.log_text.text.setText)
task.accomplish.connect(lambda: self.del_board(f"调度台_{task.name}"))
self.script_list[f"调度台_{task.name}"] = dispatch_box
self.stackedWidget.addWidget(self.script_list[f"调度台_{task.name}"])
self.pivot.addItem(routeKey=f"调度台_{task.name}", text=f"调度台 {task.name}")
def del_board(self, name: str) -> None:
"""删除指定子界面"""
self.pivot.setCurrentItem("主调度台")
self.stackedWidget.removeWidget(self.script_list[name])
self.script_list[name].deleteLater()
self.pivot.removeWidget(name)
def connect_main_board(self, task: Task) -> None:
"""连接主调度台"""
self.script_list["主调度台"].top_bar.Lable.setText(
f"{task.name} - {task.mode.replace("_主调度台","")}模式"
)
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.button.clicked.disconnect()
self.script_list["主调度台"].top_bar.button.setText("中止任务")
self.script_list["主调度台"].top_bar.button.clicked.connect(
lambda: TaskManager.stop_task(task.name)
)
task.create_task_list.connect(
self.script_list["主调度台"].info.task.create_task
)
task.create_user_list.connect(
self.script_list["主调度台"].info.user.create_user
)
task.update_task_list.connect(
self.script_list["主调度台"].info.task.update_task
)
task.update_user_list.connect(
self.script_list["主调度台"].info.user.update_user
)
task.update_log_text.connect(
self.script_list["主调度台"].info.log_text.text.setText
)
task.accomplish.connect(lambda: self.disconnect_main_board(task.name))
def disconnect_main_board(self, name: str) -> None:
"""断开主调度台"""
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.button.clicked.disconnect()
self.script_list["主调度台"].top_bar.button.setText("开始任务")
self.script_list["主调度台"].top_bar.button.clicked.connect(
self.script_list["主调度台"].top_bar.start_task
)
self.script_list["主调度台"].info.log_text.text.setText(
Config.get_history(name)["History"]
)
def update_top_bar(self):
"""更新顶栏"""
list = []
queue_numb, member_numb = 0, 0
if (Config.app_path / "config/QueueConfig").exists():
for json_file in (Config.app_path / "config/QueueConfig").glob("*.json"):
list.append(f"队列 - {json_file.stem}")
queue_numb += 1
if (Config.app_path / "config/MaaConfig").exists():
for subdir in (Config.app_path / "config/MaaConfig").iterdir():
if subdir.is_dir():
list.append(f"实例 - Maa - {subdir.name}")
member_numb += 1
self.script_list["主调度台"].top_bar.object.clear()
self.script_list["主调度台"].top_bar.object.addItems(list)
self.script_list["主调度台"].top_bar.mode.clear()
self.script_list["主调度台"].top_bar.mode.addItems(["自动代理", "人工排查"])
if queue_numb == 1:
self.script_list["主调度台"].top_bar.object.setCurrentIndex(0)
elif member_numb == 1:
self.script_list["主调度台"].top_bar.object.setCurrentIndex(queue_numb)
else:
self.script_list["主调度台"].top_bar.object.setCurrentIndex(-1)
self.script_list["主调度台"].top_bar.mode.setCurrentIndex(0)
class DispatchBox(QWidget):
def __init__(self, name: str, parent=None):
super().__init__(parent)
self.setObjectName(name)
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.button = PushButton("开始任务")
self.button.clicked.connect(self.start_task)
Layout.addWidget(self.Lable)
Layout.addWidget(self.object)
Layout.addWidget(self.mode)
Layout.addStretch(1)
Layout.addWidget(self.button)
else:
self.Lable = SubtitleLabel(name, self)
self.button = PushButton("中止任务")
Layout.addWidget(self.Lable)
Layout.addStretch(1)
Layout.addWidget(self.button)
def start_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
name = self.object.currentText().split(" - ")[-1]
if name in Config.running_list:
logger.warning(f"任务已存在:{name}")
MainInfoBar.push_info_bar("warning", "任务已存在", name, 5000)
return None
if self.object.currentText().split(" - ")[0] == "队列":
with (Config.app_path / f"config/QueueConfig/{name}.json").open(
mode="r", encoding="utf-8"
) as f:
info = json.load(f)
logger.info(f"用户添加任务:{name}")
TaskManager.add_task(f"{self.mode.currentText()}_主调度台", name, info)
elif self.object.currentText().split(" - ")[0] == "实例":
if self.object.currentText().split(" - ")[1] == "Maa":
info = {"Queue": {"Member_1": name}}
logger.info(f"用户添加任务:{name}")
TaskManager.add_task(
f"{self.mode.currentText()}_主调度台", "自定义队列", info
)
class DispatchInfoCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("调度信息")
self.task = self.TaskInfoCard(self)
self.user = self.UserInfoCard(self)
self.log_text = self.LogCard(self)
self.viewLayout.addWidget(self.task)
self.viewLayout.addWidget(self.user)
self.viewLayout.addWidget(self.log_text)
self.viewLayout.setStretch(0, 1)
self.viewLayout.setStretch(1, 1)
self.viewLayout.setStretch(2, 5)
def update_board(self, task_list: list, user_list: list, log: str):
"""更新调度信息"""
self.task.update_task(task_list)
self.user.update_user(user_list)
self.log_text.text.setText(log)
class TaskInfoCard(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.task_cards: List[StatefulItemCard] = []
def create_task(self, task_list: list):
"""创建任务队列"""
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.task_cards = []
for task in task_list:
self.task_cards.append(StatefulItemCard(task))
self.Layout.addWidget(self.task_cards[-1])
self.Layout.addStretch(1)
def update_task(self, task_list: list):
"""更新任务队列"""
for i in range(len(task_list)):
self.task_cards[i].update_status(task_list[i][1])
class UserInfoCard(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.user_cards: List[StatefulItemCard] = []
def create_user(self, user_list: list):
"""创建用户队列"""
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.user_cards = []
for user in user_list:
self.user_cards.append(StatefulItemCard(user))
self.Layout.addWidget(self.user_cards[-1])
self.Layout.addStretch(1)
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()

258
app/ui/history.py Normal file
View File

@@ -0,0 +1,258 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA历史记录界面
v4.2
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
)
from qfluentwidgets import (
ScrollArea,
FluentIcon,
HeaderCardWidget,
PushButton,
ExpandGroupSettingCard,
TextBrowser,
)
from PySide6.QtCore import Signal
import os
from functools import partial
from pathlib import Path
from typing import List
from app.core import Config
from .Widget import StatefulItemCard, QuantifiedItemCard
class History(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("历史记录")
content_widget = QWidget()
self.content_layout = QVBoxLayout(content_widget)
scrollArea = ScrollArea()
scrollArea.setWidgetResizable(True)
scrollArea.setWidget(content_widget)
layout = QVBoxLayout()
layout.addWidget(scrollArea)
self.setLayout(layout)
self.history_card_list = []
self.refresh()
def refresh(self):
"""刷新脚本实例界面"""
while self.content_layout.count() > 0:
item = self.content_layout.takeAt(0)
if item.spacerItem():
self.content_layout.removeItem(item.spacerItem())
elif item.widget():
item.widget().deleteLater()
self.history_card_list = []
history_dict = Config.search_history()
for date, user_list in history_dict.items():
self.history_card_list.append(HistoryCard(date, user_list, self))
self.content_layout.addWidget(self.history_card_list[-1])
self.content_layout.addStretch(1)
class HistoryCard(ExpandGroupSettingCard):
def __init__(self, date: str, user_list: List[Path], parent=None):
super().__init__(
FluentIcon.HISTORY, date, f"{date}的历史运行记录与统计信息", parent
)
widget = QWidget()
Layout = QVBoxLayout(widget)
self.viewLayout.setContentsMargins(0, 0, 0, 0)
self.viewLayout.setSpacing(0)
self.addGroupWidget(widget)
self.user_history_card_list = []
for user_path in user_list:
self.user_history_card_list.append(self.UserHistoryCard(user_path, self))
Layout.addWidget(self.user_history_card_list[-1])
class UserHistoryCard(HeaderCardWidget):
def __init__(
self,
user_history_path: Path,
parent=None,
):
super().__init__(parent)
self.setTitle(user_history_path.name.replace(".json", ""))
self.user_history_path = user_history_path
self.main_history = Config.load_maa_logs("总览", user_history_path)
self.index_card = self.IndexCard(self.main_history["条目索引"], self)
self.statistics_card = QHBoxLayout()
self.log_card = self.LogCard(self)
self.index_card.index_changed.connect(self.update_info)
self.viewLayout.addWidget(self.index_card)
self.viewLayout.addLayout(self.statistics_card)
self.viewLayout.addWidget(self.log_card)
self.viewLayout.setContentsMargins(0, 0, 0, 0)
self.viewLayout.setSpacing(0)
self.viewLayout.setStretch(0, 1)
self.viewLayout.setStretch(2, 4)
self.update_info("数据总览")
def update_info(self, index: str) -> None:
"""更新信息"""
if index == "数据总览":
while self.statistics_card.count() > 0:
item = self.statistics_card.takeAt(0)
if item.spacerItem():
self.statistics_card.removeItem(item.spacerItem())
elif item.widget():
item.widget().deleteLater()
for name, item_list in self.main_history["统计数据"].items():
statistics_card = self.StatisticsCard(name, item_list, self)
self.statistics_card.addWidget(statistics_card)
self.log_card.hide()
else:
single_history = Config.load_maa_logs(
"单项",
self.user_history_path.with_suffix("")
/ f"{index.replace(":","-")}.json",
)
while self.statistics_card.count() > 0:
item = self.statistics_card.takeAt(0)
if item.spacerItem():
self.statistics_card.removeItem(item.spacerItem())
elif item.widget():
item.widget().deleteLater()
for name, item_list in single_history["统计数据"].items():
statistics_card = self.StatisticsCard(name, item_list, self)
self.statistics_card.addWidget(statistics_card)
self.log_card.text.setText(single_history["日志信息"])
self.log_card.button.clicked.disconnect()
self.log_card.button.clicked.connect(
lambda: os.startfile(
self.user_history_path.with_suffix("")
/ f"{index.replace(":","-")}.log"
)
)
self.log_card.show()
self.viewLayout.setStretch(1, self.statistics_card.count())
self.setMinimumHeight(300)
class IndexCard(HeaderCardWidget):
index_changed = Signal(str)
def __init__(self, index_list: list, parent=None):
super().__init__(parent)
self.setTitle("记录条目")
self.Layout = QVBoxLayout()
self.viewLayout.addLayout(self.Layout)
self.viewLayout.setContentsMargins(3, 0, 3, 3)
self.index_cards: List[StatefulItemCard] = []
for index in index_list:
self.index_cards.append(StatefulItemCard(index))
self.index_cards[-1].clicked.connect(
partial(self.index_changed.emit, index[0])
)
self.Layout.addWidget(self.index_cards[-1])
self.Layout.addStretch(1)
class StatisticsCard(HeaderCardWidget):
def __init__(self, name: str, item_list: list, parent=None):
super().__init__(parent)
self.setTitle(name)
self.Layout = QVBoxLayout()
self.viewLayout.addLayout(self.Layout)
self.viewLayout.setContentsMargins(3, 0, 3, 3)
self.item_cards: List[QuantifiedItemCard] = []
for item in item_list:
self.item_cards.append(QuantifiedItemCard(item))
self.Layout.addWidget(self.item_cards[-1])
if len(item_list) == 0:
self.Layout.addWidget(QuantifiedItemCard(["暂无记录", ""]))
self.Layout.addStretch(1)
class LogCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("日志")
self.text = TextBrowser(self)
self.button = PushButton("打开日志文件", self)
self.button.clicked.connect(lambda: print("打开日志文件"))
Layout = QVBoxLayout()
Layout.addWidget(self.text)
Layout.addWidget(self.button)
self.viewLayout.setContentsMargins(3, 0, 3, 3)
self.viewLayout.addLayout(Layout)

423
app/ui/home.py Normal file
View File

@@ -0,0 +1,423 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA主界面
v4.2
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QSpacerItem,
QSizePolicy,
QFileDialog,
)
from PySide6.QtCore import Qt, QSize, QUrl
from PySide6.QtGui import QDesktopServices, QColor
from qfluentwidgets import (
FluentIcon,
ScrollArea,
SimpleCardWidget,
PrimaryToolButton,
TextBrowser,
)
import re
import shutil
import requests
import json
import time
from datetime import datetime
from pathlib import Path
from app.core import Config, MainInfoBar
from .Widget import Banner, IconButton
class Home(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("主页")
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)
v_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# 空白占位符
v_layout.addItem(
QSpacerItem(10, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum)
)
# 顶部部分 (按钮组)
h1_layout = QHBoxLayout()
h1_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# 左边留白区域
h1_layout.addStretch()
# 按钮组
buttonGroup = ButtonGroup()
buttonGroup.setMaximumHeight(320)
h1_layout.addWidget(buttonGroup)
# 空白占位符
h1_layout.addItem(
QSpacerItem(20, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum)
)
# 将顶部水平布局添加到垂直布局
v_layout.addLayout(h1_layout)
# 中间留白区域
v_layout.addItem(
QSpacerItem(10, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum)
)
v_layout.addStretch()
# 中间留白区域
v_layout.addItem(
QSpacerItem(10, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum)
)
v_layout.addStretch()
# 底部部分 (图片切换按钮)
h2_layout = QHBoxLayout()
h2_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# 左边留白区域
h2_layout.addItem(
QSpacerItem(20, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum)
)
# # 公告卡片
# noticeCard = NoticeCard()
# h2_layout.addWidget(noticeCard)
h2_layout.addStretch()
# 自定义图像按钮布局
self.imageButton = PrimaryToolButton(FluentIcon.IMAGE_EXPORT)
self.imageButton.setFixedSize(56, 56)
self.imageButton.setIconSize(QSize(32, 32))
self.imageButton.clicked.connect(self.get_home_image)
v1_layout = QVBoxLayout()
v1_layout.addWidget(self.imageButton, alignment=Qt.AlignmentFlag.AlignBottom)
h2_layout.addLayout(v1_layout)
# 空白占位符
h2_layout.addItem(
QSpacerItem(25, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum)
)
# 将底部水平布局添加到垂直布局
v_layout.addLayout(h2_layout)
layout = QVBoxLayout()
scrollArea = ScrollArea()
scrollArea.setWidgetResizable(True)
scrollArea.setWidget(widget)
layout.addWidget(scrollArea)
self.setLayout(layout)
self.set_banner()
def get_home_image(self) -> None:
"""获取主页图片"""
if (
Config.global_config.get(Config.global_config.function_HomeImageMode)
== "默认"
):
pass
elif (
Config.global_config.get(Config.global_config.function_HomeImageMode)
== "自定义"
):
file_path, _ = QFileDialog.getOpenFileName(
self, "打开自定义主页图片", "", "图片文件 (*.png *.jpg *.bmp)"
)
if file_path:
for file in Config.app_path.glob(
"resources/images/Home/BannerCustomize.*"
):
file.unlink()
shutil.copy(
file_path,
Config.app_path
/ f"resources/images/Home/BannerCustomize{Path(file_path).suffix}",
)
logger.info(f"自定义主页图片更换成功:{file_path}")
MainInfoBar.push_info_bar(
"success",
"主页图片更换成功",
"自定义主页图片更换成功!",
3000,
)
else:
logger.warning("自定义主页图片更换失败:未选择图片文件")
MainInfoBar.push_info_bar(
"warning",
"主页图片更换失败",
"未选择图片文件!",
5000,
)
elif (
Config.global_config.get(Config.global_config.function_HomeImageMode)
== "主题图像"
):
# 从远程服务器获取最新主题图像
for _ in range(3):
try:
response = requests.get(
"https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/theme_image.json"
)
theme_image = response.json()
break
except Exception as e:
err = e
time.sleep(0.1)
else:
logger.error(f"获取最新主题图像时出错:\n{err}")
MainInfoBar.push_info_bar(
"error",
"主题图像获取失败",
f"获取最新主题图像信息时出错:\n{err}",
-1,
)
return None
if (Config.app_path / "resources/theme_image.json").exists():
with (Config.app_path / "resources/theme_image.json").open(
mode="r", encoding="utf-8"
) as f:
theme_image_local = json.load(f)
time_local = datetime.strptime(
theme_image_local["time"], "%Y-%m-%d %H:%M"
)
else:
time_local = datetime.strptime("2000-01-01 00:00", "%Y-%m-%d %H:%M")
if not (
Config.app_path / "resources/images/Home/BannerTheme.jpg"
).exists() or (
datetime.now()
> datetime.strptime(theme_image["time"], "%Y-%m-%d %H:%M")
and datetime.strptime(theme_image["time"], "%Y-%m-%d %H:%M")
> time_local
):
response = requests.get(theme_image["url"])
if response.status_code == 200:
with open(
Config.app_path / "resources/images/Home/BannerTheme.jpg", "wb"
) as file:
file.write(response.content)
logger.info(f"主题图像「{theme_image["name"]}」下载成功")
MainInfoBar.push_info_bar(
"success",
"主题图像下载成功",
f"{theme_image["name"]}」下载成功!",
3000,
)
else:
logger.error("主题图像下载失败")
MainInfoBar.push_info_bar(
"error",
"主题图像下载失败",
f"主题图像下载失败:{response.status_code}",
-1,
)
with (Config.app_path / "resources/theme_image.json").open(
mode="w", encoding="utf-8"
) as f:
json.dump(theme_image, f, ensure_ascii=False, indent=4)
else:
logger.info("主题图像已是最新")
MainInfoBar.push_info_bar(
"info",
"主题图像已是最新",
"主题图像已是最新!",
3000,
)
self.set_banner()
def set_banner(self):
"""设置主页图像"""
if (
Config.global_config.get(Config.global_config.function_HomeImageMode)
== "默认"
):
self.banner.set_banner_image(
str(Config.app_path / "resources/images/Home/BannerDefault.png")
)
self.imageButton.hide()
self.banner_text.setVisible(False)
elif (
Config.global_config.get(Config.global_config.function_HomeImageMode)
== "自定义"
):
for file in Config.app_path.glob("resources/images/Home/BannerCustomize.*"):
self.banner.set_banner_image(str(file))
break
self.imageButton.show()
self.banner_text.setVisible(False)
elif (
Config.global_config.get(Config.global_config.function_HomeImageMode)
== "主题图像"
):
self.banner.set_banner_image(
str(Config.app_path / "resources/images/Home/BannerTheme.jpg")
)
self.imageButton.show()
self.banner_text.setVisible(True)
if (Config.app_path / "resources/theme_image.json").exists():
with (Config.app_path / "resources/theme_image.json").open(
mode="r", encoding="utf-8"
) as f:
theme_image = json.load(f)
html_content = theme_image["html"]
else:
html_content = "<h1>主题图像</h1><p>主题图像信息未知</p>"
self.banner_text.setHtml(re.sub(r"<img[^>]*>", "", html_content))
class ButtonGroup(SimpleCardWidget):
"""显示主页和 GitHub 按钮的竖直按钮组"""
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setFixedSize(56, 180)
layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# 创建主页按钮
home_button = IconButton(
FluentIcon.HOME.icon(color=QColor("#fff")),
tip_title="AUTO_MAA官网",
tip_content="AUTO_MAA官方文档站",
isTooltip=True,
)
home_button.setIconSize(QSize(32, 32))
home_button.clicked.connect(self.open_home)
layout.addWidget(home_button)
# 创建 GitHub 按钮
github_button = IconButton(
FluentIcon.GITHUB.icon(color=QColor("#fff")),
tip_title="Github仓库",
tip_content="如果本项目有帮助到您~\n不妨给项目点一个Star⭐",
isTooltip=True,
)
github_button.setIconSize(QSize(32, 32))
github_button.clicked.connect(self.open_github)
layout.addWidget(github_button)
# # 创建 文档 按钮
# doc_button = IconButton(
# FluentIcon.DICTIONARY.icon(color=QColor("#fff")),
# tip_title="自助排障文档",
# tip_content="点击打开自助排障文档,好孩子都能看懂",
# isTooltip=True,
# )
# doc_button.setIconSize(QSize(32, 32))
# doc_button.clicked.connect(self.open_doc)
# layout.addWidget(doc_button)
# 创建 Q群 按钮
doc_button = IconButton(
FluentIcon.CHAT.icon(color=QColor("#fff")),
tip_title="官方社群",
tip_content="加入官方群聊【AUTO_MAA绝赞DeBug中",
isTooltip=True,
)
doc_button.setIconSize(QSize(32, 32))
doc_button.clicked.connect(self.open_chat)
layout.addWidget(doc_button)
# 创建 官方店铺 按钮 (当然没有)
doc_button = IconButton(
FluentIcon.SHOPPING_CART.icon(color=QColor("#fff")),
tip_title="官方店铺",
tip_content="暂时没有官方店铺,但是可以加入官方群聊哦~",
isTooltip=True,
)
doc_button.setIconSize(QSize(32, 32))
doc_button.clicked.connect(self.open_sales)
layout.addWidget(doc_button)
def _normalBackgroundColor(self):
return QColor(0, 0, 0, 96)
def open_home(self):
"""打开主页链接"""
QDesktopServices.openUrl(QUrl("https://clozya.github.io/AUTOMAA_docs"))
def open_github(self):
"""打开 GitHub 链接"""
QDesktopServices.openUrl(QUrl("https://github.com/DLmaster361/AUTO_MAA"))
def open_chat(self):
"""打开 Q群 链接"""
QDesktopServices.openUrl(QUrl("https://qm.qq.com/q/bd9fISNoME"))
def open_doc(self):
"""打开 文档 链接"""
QDesktopServices.openUrl(QUrl("https://clozya.github.io/AUTOMAA_docs"))
def open_sales(self):
"""其实还是打开 Q群 链接"""
QDesktopServices.openUrl(QUrl("https://qm.qq.com/q/bd9fISNoME"))

435
app/ui/main_window.py Normal file
View File

@@ -0,0 +1,435 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA主界面
v4.2
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import QSystemTrayIcon
from qfluentwidgets import (
Action,
PushButton,
SystemTrayMenu,
SplashScreen,
FluentIcon,
InfoBar,
InfoBarPosition,
setTheme,
isDarkTheme,
SystemThemeListener,
Theme,
MSFluentWindow,
NavigationItemPosition,
qconfig,
)
from PySide6.QtGui import QIcon, QCloseEvent
from PySide6.QtCore import Qt, QTimer
import json
from datetime import datetime, timedelta
import shutil
from app.core import Config, TaskManager, MainTimer, MainInfoBar
from app.services import Notify, Crypto, System
from .home import Home
from .member_manager import MemberManager
from .queue_manager import QueueManager
from .dispatch_center import DispatchCenter
from .history import History
from .setting import Setting
class AUTO_MAA(MSFluentWindow):
def __init__(self):
super().__init__()
self.setWindowIcon(QIcon(str(Config.app_path / "resources/icons/AUTO_MAA.ico")))
self.setWindowTitle("AUTO_MAA")
setTheme(Theme.AUTO, lazy=True)
self.splashScreen = SplashScreen(self.windowIcon(), self)
self.show_ui("显示主窗口", if_quick=True)
MainInfoBar.main_window = self.window()
System.main_window = self.window()
# 创建主窗口
self.home = Home(self)
self.member_manager = MemberManager(self)
self.queue_manager = QueueManager(self)
self.dispatch_center = DispatchCenter(self)
self.history = History(self)
self.setting = Setting(self)
self.addSubInterface(
self.home,
FluentIcon.HOME,
"主页",
FluentIcon.HOME,
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.member_manager,
FluentIcon.ROBOT,
"脚本管理",
FluentIcon.ROBOT,
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.queue_manager,
FluentIcon.BOOK_SHELF,
"调度队列",
FluentIcon.BOOK_SHELF,
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.dispatch_center,
FluentIcon.IOT,
"调度中心",
FluentIcon.IOT,
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.history,
FluentIcon.HISTORY,
"历史记录",
FluentIcon.HISTORY,
NavigationItemPosition.BOTTOM,
)
self.addSubInterface(
self.setting,
FluentIcon.SETTING,
"设置",
FluentIcon.SETTING,
NavigationItemPosition.BOTTOM,
)
self.stackedWidget.currentChanged.connect(
lambda index: (self.member_manager.refresh() if index == 1 else None)
)
self.stackedWidget.currentChanged.connect(
lambda index: self.queue_manager.refresh() 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(
lambda index: (self.history.refresh() if index == 4 else None)
)
# 创建系统托盘及其菜单
self.tray = QSystemTrayIcon(
QIcon(str(Config.app_path / "resources/icons/AUTO_MAA.ico")), self
)
self.tray.setToolTip("AUTO_MAA")
self.tray_menu = SystemTrayMenu("AUTO_MAA", self)
# 显示主界面菜单项
self.tray_menu.addAction(
Action(
FluentIcon.CAFE,
"显示主界面",
triggered=lambda: self.show_ui("显示主窗口"),
)
)
self.tray_menu.addSeparator()
# 开始任务菜单项
self.tray_menu.addActions(
[
Action(FluentIcon.PLAY, "运行自动代理", triggered=self.start_main_task),
Action(
FluentIcon.PAUSE,
"中止所有任务",
triggered=lambda: TaskManager.stop_task("ALL"),
),
]
)
self.tray_menu.addSeparator()
# 退出主程序菜单项
self.tray_menu.addAction(
Action(FluentIcon.POWER_BUTTON, "退出主程序", triggered=self.window().close)
)
# 设置托盘菜单
self.tray.setContextMenu(self.tray_menu)
self.tray.activated.connect(self.on_tray_activated)
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)
self.setting.ui.card_IfShowTray.checkedChanged.connect(
lambda: self.show_ui("配置托盘")
)
self.setting.ui.card_IfToTray.checkedChanged.connect(self.set_min_method)
self.setting.function.card_HomeImageMode.comboBox.currentIndexChanged.connect(
lambda index: (
self.home.get_home_image() if index == 2 else self.home.set_banner()
)
)
self.splashScreen.finish()
self.themeListener = SystemThemeListener(self)
self.themeListener.systemThemeChanged.connect(self.switch_theme)
self.themeListener.start()
def switch_theme(self):
"""切换主题"""
setTheme(Theme.AUTO, lazy=True)
QTimer.singleShot(100, lambda: setTheme(Theme.AUTO, lazy=True))
# 云母特效启用时需要增加重试机制
if self.isMicaEffectEnabled():
QTimer.singleShot(
100,
lambda: self.windowEffect.setMicaEffect(self.winId(), isDarkTheme()),
)
def start_up_task(self) -> None:
"""启动时任务"""
# 加载配置
qconfig.load(Config.config_path, Config.global_config)
Config.global_config.save()
# 清理旧日志
self.clean_old_logs()
# 检查密码
self.setting.check_PASSWORD()
# 获取主题图像
if (
Config.global_config.get(Config.global_config.function_HomeImageMode)
== "主题图像"
):
self.home.get_home_image()
# 获取公告
self.setting.show_notice(if_show=False)
# 检查更新
if Config.global_config.get(Config.global_config.update_IfAutoUpdate):
result = self.setting.get_update_info()
if result == "已是最新版本~":
MainInfoBar.push_info_bar("success", "更新检查", result, 3000)
else:
info = InfoBar.info(
title="更新检查",
content=result,
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.BOTTOM_LEFT,
duration=-1,
parent=self,
)
Up = PushButton("更新")
Up.clicked.connect(lambda: self.setting.get_update(if_question=False))
Up.clicked.connect(info.close)
info.addWidget(Up)
info.show()
# 直接运行主任务
if Config.global_config.get(Config.global_config.start_IfRunDirectly):
self.start_main_task()
# 直接最小化
if Config.global_config.get(Config.global_config.start_IfMinimizeDirectly):
self.titleBar.minBtn.click()
def set_min_method(self) -> None:
"""设置最小化方法"""
if Config.global_config.get(Config.global_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 clean_old_logs(self):
"""
删除超过用户设定天数的日志文件(基于目录日期)
"""
if (
Config.global_config.get(Config.global_config.function_HistoryRetentionTime)
== 0
):
logger.info("由于用户设置日志永久保留,跳过日志清理")
return
deleted_count = 0
for date_folder in (Config.app_path / "history").iterdir():
if not date_folder.is_dir():
continue # 只处理日期文件夹
try:
# 只检查 `YYYY-MM-DD` 格式的文件夹
folder_date = datetime.strptime(date_folder.name, "%Y-%m-%d")
if datetime.now() - folder_date > timedelta(
days=Config.global_config.get(
Config.global_config.function_HistoryRetentionTime
)
):
shutil.rmtree(date_folder, ignore_errors=True)
deleted_count += 1
logger.info(f"已删除超期日志目录: {date_folder}")
except ValueError:
logger.warning(f"非日期格式的目录: {date_folder}")
logger.info(f"清理完成: {deleted_count} 个日期目录")
def start_main_task(self) -> None:
"""启动主任务"""
if (Config.app_path / "config/QueueConfig/调度队列_1.json").exists():
with (Config.app_path / "config/QueueConfig/调度队列_1.json").open(
mode="r", encoding="utf-8"
) as f:
info = json.load(f)
logger.info("自动添加任务调度队列_1")
TaskManager.add_task("自动代理_主调度台", "主任务队列", info)
elif (Config.app_path / "config/MaaConfig/脚本_1").exists():
info = {"Queue": {"Member_1": "脚本_1"}}
logger.info("自动添加任务脚本_1")
TaskManager.add_task("自动代理_主调度台", "主任务队列", info)
else:
logger.worning("启动主任务失败:未找到有效的主任务配置文件")
MainInfoBar.push_info_bar(
"warning", "启动主任务失败", "“调度队列_1”与“脚本_1”均不存在", -1
)
def show_ui(self, mode: str, if_quick: bool = False) -> None:
"""配置窗口状态"""
if mode == "显示主窗口":
# 配置主窗口
size = list(
map(
int,
Config.global_config.get(Config.global_config.ui_size).split("x"),
)
)
location = list(
map(
int,
Config.global_config.get(Config.global_config.ui_location).split(
"x"
),
)
)
self.window().setGeometry(location[0], location[1], size[0], size[1])
self.window().show()
self.window().raise_()
self.window().activateWindow()
if not if_quick:
if Config.global_config.get(Config.global_config.ui_maximized):
self.window().showMaximized()
self.set_min_method()
self.show_ui("配置托盘")
elif mode == "配置托盘":
if Config.global_config.get(Config.global_config.ui_IfShowTray):
self.tray.show()
else:
self.tray.hide()
elif mode == "隐藏到托盘":
# 保存窗口相关属性
if not self.window().isMaximized():
Config.global_config.set(
Config.global_config.ui_size,
f"{self.geometry().width()}x{self.geometry().height()}",
)
Config.global_config.set(
Config.global_config.ui_location,
f"{self.geometry().x()}x{self.geometry().y()}",
)
Config.global_config.set(
Config.global_config.ui_maximized, self.window().isMaximized()
)
Config.global_config.save()
# 隐藏主窗口
if not if_quick:
self.window().hide()
self.tray.show()
def closeEvent(self, event: QCloseEvent):
"""清理残余进程"""
self.show_ui("隐藏到托盘", if_quick=True)
# 清理各功能线程
MainTimer.Timer.stop()
MainTimer.Timer.deleteLater()
TaskManager.stop_task("ALL")
# 关闭数据库连接
Config.close_database()
# 关闭主题监听
self.themeListener.terminate()
self.themeListener.deleteLater()
logger.info("AUTO_MAA主程序关闭")
logger.info("----------------END----------------")
event.accept()

1697
app/ui/member_manager.py Normal file

File diff suppressed because it is too large Load Diff

669
app/ui/queue_manager.py Normal file
View File

@@ -0,0 +1,669 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA调度队列界面
v4.2
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QStackedWidget,
QHBoxLayout,
)
from qfluentwidgets import (
Action,
qconfig,
Pivot,
ScrollArea,
FluentIcon,
MessageBox,
HeaderCardWidget,
TextBrowser,
CommandBar,
SwitchSettingCard,
ComboBoxSettingCard,
)
from PySide6.QtCore import Qt
from typing import List
import json
import shutil
from app.core import Config, MainInfoBar
from .Widget import (
LineEditSettingCard,
TimeEditSettingCard,
NoOptionComboBoxSettingCard,
)
class QueueManager(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("调度队列")
layout = QVBoxLayout(self)
self.tools = CommandBar()
self.queue_manager = QueueSettingBox(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
),
]
)
layout.addWidget(self.tools)
layout.addWidget(self.queue_manager)
def add_setting_box(self):
"""添加一个调度队列"""
index = len(self.queue_manager.search_queue()) + 1
qconfig.load(
Config.app_path / f"config/QueueConfig/调度队列_{index}.json",
Config.queue_config,
)
Config.clear_queue_config()
Config.queue_config.save()
self.queue_manager.add_QueueSettingBox(index)
self.queue_manager.switch_SettingBox(index)
def del_setting_box(self):
"""删除一个调度队列实例"""
name = self.queue_manager.pivot.currentRouteKey()
if name == None:
logger.warning("未选择调度队列")
MainInfoBar.push_info_bar(
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
)
return None
if name in Config.running_list:
logger.warning("调度队列正在运行")
MainInfoBar.push_info_bar(
"warning", "调度队列正在运行", "请先停止调度队列", 5000
)
return None
choice = MessageBox(
"确认",
f"确定要删除 {name} 吗?",
self.window(),
)
if choice.exec():
queue_list = self.queue_manager.search_queue()
move_list = [_ for _ in queue_list if int(_[0][5:]) > int(name[5:])]
index = max(int(name[5:]) - 1, 1)
self.queue_manager.clear_SettingBox()
(Config.app_path / f"config/QueueConfig/{name}.json").unlink()
for queue in move_list:
if (Config.app_path / f"config/QueueConfig/{queue[0]}.json").exists():
(Config.app_path / f"config/QueueConfig/{queue[0]}.json").rename(
Config.app_path
/ f"config/QueueConfig/调度队列_{int(queue[0][5:])-1}.json",
)
self.queue_manager.show_SettingBox(index)
def left_setting_box(self):
"""向左移动调度队列实例"""
name = self.queue_manager.pivot.currentRouteKey()
if name == None:
logger.warning("未选择调度队列")
MainInfoBar.push_info_bar(
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
)
return None
index = int(name[5:])
if index == 1:
logger.warning("向左移动调度队列时已到达最左端")
MainInfoBar.push_info_bar(
"warning", "已经是第一个调度队列", "无法向左移动", 5000
)
return None
if name in Config.running_list or f"调度队列_{index-1}" in Config.running_list:
logger.warning("相关调度队列正在运行")
MainInfoBar.push_info_bar(
"warning", "相关调度队列正在运行", "请先停止调度队列", 5000
)
return None
self.queue_manager.clear_SettingBox()
(Config.app_path / f"config/QueueConfig/调度队列_{index}.json").rename(
Config.app_path / f"config/QueueConfig/调度队列_0.json",
)
shutil.move(
str(Config.app_path / f"config/QueueConfig/调度队列_{index-1}.json"),
str(Config.app_path / f"config/QueueConfig/调度队列_{index}.json"),
)
(Config.app_path / f"config/QueueConfig/调度队列_0.json").rename(
Config.app_path / f"config/QueueConfig/调度队列_{index-1}.json",
)
self.queue_manager.show_SettingBox(index - 1)
def right_setting_box(self):
"""向右移动调度队列实例"""
name = self.queue_manager.pivot.currentRouteKey()
if name == None:
logger.warning("未选择调度队列")
MainInfoBar.push_info_bar(
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
)
return None
queue_list = self.queue_manager.search_queue()
index = int(name[5:])
if index == len(queue_list):
logger.warning("向右移动调度队列时已到达最右端")
MainInfoBar.push_info_bar(
"warning", "已经是最后一个调度队列", "无法向右移动", 5000
)
return None
if name in Config.running_list or f"调度队列_{index+1}" in Config.running_list:
logger.warning("相关调度队列正在运行")
MainInfoBar.push_info_bar(
"warning", "相关调度队列正在运行", "请先停止调度队列", 5000
)
return None
self.queue_manager.clear_SettingBox()
(Config.app_path / f"config/QueueConfig/调度队列_{index}.json").rename(
Config.app_path / f"config/QueueConfig/调度队列_0.json",
)
(Config.app_path / f"config/QueueConfig/调度队列_{index+1}.json").rename(
Config.app_path / f"config/QueueConfig/调度队列_{index}.json",
)
(Config.app_path / f"config/QueueConfig/调度队列_0.json").rename(
Config.app_path / f"config/QueueConfig/调度队列_{index+1}.json",
)
self.queue_manager.show_SettingBox(index + 1)
def refresh(self):
"""刷新调度队列界面"""
if len(self.queue_manager.search_queue()) == 0:
index = 0
else:
index = int(self.queue_manager.pivot.currentRouteKey()[5:])
self.queue_manager.clear_SettingBox()
self.queue_manager.show_SettingBox(index)
class QueueSettingBox(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("调度队列管理")
self.pivot = Pivot(self)
self.stackedWidget = QStackedWidget(self)
self.Layout = QVBoxLayout(self)
self.script_list: List[QueueMemberSettingBox] = []
self.Layout.addWidget(self.pivot, 0, Qt.AlignHCenter)
self.Layout.addWidget(self.stackedWidget)
self.Layout.setContentsMargins(0, 0, 0, 0)
self.pivot.currentItemChanged.connect(
lambda index: self.switch_SettingBox(int(index[5:]), if_change_pivot=False)
)
self.show_SettingBox(1)
def show_SettingBox(self, index) -> None:
"""加载所有子界面"""
queue_list = self.search_queue()
qconfig.load(
Config.app_path / "config/临时.json",
Config.queue_config,
)
Config.clear_queue_config()
for queue in queue_list:
self.add_QueueSettingBox(int(queue[0][5:]))
if (Config.app_path / "config/临时.json").exists():
(Config.app_path / "config/临时.json").unlink()
self.switch_SettingBox(index)
def switch_SettingBox(self, index: int, if_change_pivot: bool = True) -> None:
"""切换到指定的子界面"""
queue_list = self.search_queue()
if len(queue_list) == 0:
return None
if index > len(queue_list):
return None
qconfig.load(
Config.app_path
/ f"config/QueueConfig/{self.script_list[index-1].objectName()}.json",
Config.queue_config,
)
if if_change_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:
self.stackedWidget.removeWidget(sub_interface)
sub_interface.deleteLater()
self.script_list.clear()
self.pivot.clear()
qconfig.load(
Config.app_path / "config/临时.json",
Config.queue_config,
)
Config.clear_queue_config()
if (Config.app_path / "config/临时.json").exists():
(Config.app_path / "config/临时.json").unlink()
def add_QueueSettingBox(self, uid: int) -> None:
"""添加一个调度队列设置界面"""
maa_setting_box = QueueMemberSettingBox(uid, self)
self.script_list.append(maa_setting_box)
self.stackedWidget.addWidget(self.script_list[-1])
self.pivot.addItem(routeKey=f"调度队列_{uid}", text=f"调度队列 {uid}")
def search_queue(self) -> list:
"""搜索所有调度队列实例"""
queue_list = []
if (Config.app_path / "config/QueueConfig").exists():
for json_file in (Config.app_path / "config/QueueConfig").glob("*.json"):
with json_file.open("r", encoding="utf-8") as f:
info = json.load(f)
queue_list.append([json_file.stem, info["QueueSet"]["Name"]])
return queue_list
class QueueMemberSettingBox(QWidget):
def __init__(self, uid: int, parent=None):
super().__init__(parent)
self.setObjectName(f"调度队列_{uid}")
layout = QVBoxLayout()
scrollArea = ScrollArea()
scrollArea.setWidgetResizable(True)
content_widget = QWidget()
content_layout = QVBoxLayout(content_widget)
self.queue_set = self.QueueSetSettingCard(self)
self.time = self.TimeSettingCard(self)
self.task = self.TaskSettingCard(self)
self.history = self.HistoryCard(self, f"调度队列_{uid}")
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.setWidget(content_widget)
layout.addWidget(scrollArea)
self.setLayout(layout)
class QueueSetSettingCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("队列设置")
Layout = QVBoxLayout()
self.card_Name = LineEditSettingCard(
"请输入调度队列名称",
FluentIcon.EDIT,
"调度队列名称",
"用于标识调度队列的名称",
Config.queue_config.queueSet_Name,
)
self.card_Enable = SwitchSettingCard(
FluentIcon.HOME,
"状态",
"调度队列状态",
Config.queue_config.queueSet_Enabled,
)
self.card_AfterAccomplish = ComboBoxSettingCard(
configItem=Config.queue_config.queueSet_AfterAccomplish,
icon=FluentIcon.POWER_BUTTON,
title="调度队列结束后",
content="选择调度队列结束后的操作",
texts=[
"无动作",
"退出AUTO_MAA",
"睡眠win系统需禁用休眠",
"休眠",
"关机",
],
)
Layout.addWidget(self.card_Name)
Layout.addWidget(self.card_Enable)
Layout.addWidget(self.card_AfterAccomplish)
self.viewLayout.addLayout(Layout)
class TimeSettingCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("定时设置")
widget_1 = QWidget()
Layout_1 = QVBoxLayout(widget_1)
widget_2 = QWidget()
Layout_2 = QVBoxLayout(widget_2)
Layout = QHBoxLayout()
self.card_Time_0 = TimeEditSettingCard(
FluentIcon.STOP_WATCH,
"定时 1",
"",
Config.queue_config.time_TimeEnabled_0,
Config.queue_config.time_TimeSet_0,
)
self.card_Time_1 = TimeEditSettingCard(
FluentIcon.STOP_WATCH,
"定时 2",
"",
Config.queue_config.time_TimeEnabled_1,
Config.queue_config.time_TimeSet_1,
)
self.card_Time_2 = TimeEditSettingCard(
FluentIcon.STOP_WATCH,
"定时 3",
"",
Config.queue_config.time_TimeEnabled_2,
Config.queue_config.time_TimeSet_2,
)
self.card_Time_3 = TimeEditSettingCard(
FluentIcon.STOP_WATCH,
"定时 4",
"",
Config.queue_config.time_TimeEnabled_3,
Config.queue_config.time_TimeSet_3,
)
self.card_Time_4 = TimeEditSettingCard(
FluentIcon.STOP_WATCH,
"定时 5",
"",
Config.queue_config.time_TimeEnabled_4,
Config.queue_config.time_TimeSet_4,
)
self.card_Time_5 = TimeEditSettingCard(
FluentIcon.STOP_WATCH,
"定时 6",
"",
Config.queue_config.time_TimeEnabled_5,
Config.queue_config.time_TimeSet_5,
)
self.card_Time_6 = TimeEditSettingCard(
FluentIcon.STOP_WATCH,
"定时 7",
"",
Config.queue_config.time_TimeEnabled_6,
Config.queue_config.time_TimeSet_6,
)
self.card_Time_7 = TimeEditSettingCard(
FluentIcon.STOP_WATCH,
"定时 8",
"",
Config.queue_config.time_TimeEnabled_7,
Config.queue_config.time_TimeSet_7,
)
self.card_Time_8 = TimeEditSettingCard(
FluentIcon.STOP_WATCH,
"定时 9",
"",
Config.queue_config.time_TimeEnabled_8,
Config.queue_config.time_TimeSet_8,
)
self.card_Time_9 = TimeEditSettingCard(
FluentIcon.STOP_WATCH,
"定时 10",
"",
Config.queue_config.time_TimeEnabled_9,
Config.queue_config.time_TimeSet_9,
)
Layout_1.addWidget(self.card_Time_0)
Layout_1.addWidget(self.card_Time_1)
Layout_1.addWidget(self.card_Time_2)
Layout_1.addWidget(self.card_Time_3)
Layout_1.addWidget(self.card_Time_4)
Layout_2.addWidget(self.card_Time_5)
Layout_2.addWidget(self.card_Time_6)
Layout_2.addWidget(self.card_Time_7)
Layout_2.addWidget(self.card_Time_8)
Layout_2.addWidget(self.card_Time_9)
Layout.addWidget(widget_1)
Layout.addWidget(widget_2)
self.viewLayout.addLayout(Layout)
class TaskSettingCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("任务队列")
Layout = QVBoxLayout()
member_list = self.search_member()
self.card_Member_1 = NoOptionComboBoxSettingCard(
Config.queue_config.queue_Member_1,
FluentIcon.APPLICATION,
"任务实例 1",
"第一个调起的脚本任务实例",
member_list[0],
member_list[1],
)
self.card_Member_2 = NoOptionComboBoxSettingCard(
Config.queue_config.queue_Member_2,
FluentIcon.APPLICATION,
"任务实例 2",
"第二个调起的脚本任务实例",
member_list[0],
member_list[1],
)
self.card_Member_3 = NoOptionComboBoxSettingCard(
Config.queue_config.queue_Member_3,
FluentIcon.APPLICATION,
"任务实例 3",
"第三个调起的脚本任务实例",
member_list[0],
member_list[1],
)
self.card_Member_4 = NoOptionComboBoxSettingCard(
Config.queue_config.queue_Member_4,
FluentIcon.APPLICATION,
"任务实例 4",
"第四个调起的脚本任务实例",
member_list[0],
member_list[1],
)
self.card_Member_5 = NoOptionComboBoxSettingCard(
Config.queue_config.queue_Member_5,
FluentIcon.APPLICATION,
"任务实例 5",
"第五个调起的脚本任务实例",
member_list[0],
member_list[1],
)
self.card_Member_6 = NoOptionComboBoxSettingCard(
Config.queue_config.queue_Member_6,
FluentIcon.APPLICATION,
"任务实例 6",
"第六个调起的脚本任务实例",
member_list[0],
member_list[1],
)
self.card_Member_7 = NoOptionComboBoxSettingCard(
Config.queue_config.queue_Member_7,
FluentIcon.APPLICATION,
"任务实例 7",
"第七个调起的脚本任务实例",
member_list[0],
member_list[1],
)
self.card_Member_8 = NoOptionComboBoxSettingCard(
Config.queue_config.queue_Member_8,
FluentIcon.APPLICATION,
"任务实例 8",
"第八个调起的脚本任务实例",
member_list[0],
member_list[1],
)
self.card_Member_9 = NoOptionComboBoxSettingCard(
Config.queue_config.queue_Member_9,
FluentIcon.APPLICATION,
"任务实例 9",
"第九个调起的脚本任务实例",
member_list[0],
member_list[1],
)
self.card_Member_10 = NoOptionComboBoxSettingCard(
Config.queue_config.queue_Member_10,
FluentIcon.APPLICATION,
"任务实例 10",
"第十个调起的脚本任务实例",
member_list[0],
member_list[1],
)
Layout.addWidget(self.card_Member_1)
Layout.addWidget(self.card_Member_2)
Layout.addWidget(self.card_Member_3)
Layout.addWidget(self.card_Member_4)
Layout.addWidget(self.card_Member_5)
Layout.addWidget(self.card_Member_6)
Layout.addWidget(self.card_Member_7)
Layout.addWidget(self.card_Member_8)
Layout.addWidget(self.card_Member_9)
Layout.addWidget(self.card_Member_10)
self.viewLayout.addLayout(Layout)
def search_member(self) -> list:
"""搜索所有脚本实例"""
member_list_name = ["禁用"]
member_list_text = ["未启用"]
if (Config.app_path / "config/MaaConfig").exists():
for subdir in (Config.app_path / "config/MaaConfig").iterdir():
if subdir.is_dir():
member_list_name.append(subdir.name)
with (subdir / "config.json").open("r", encoding="utf-8") as f:
info = json.load(f)
if info["MaaSet"]["Name"] != "":
member_list_text.append(
f"{subdir.name} - {info["MaaSet"]["Name"]}"
)
else:
member_list_text.append(subdir.name)
return [member_list_name, member_list_text]
class HistoryCard(HeaderCardWidget):
def __init__(self, parent=None, name: str = None):
super().__init__(parent)
self.setTitle("历史运行记录")
self.text = TextBrowser()
self.text.setMinimumHeight(300)
history = Config.get_history(name)
self.text.setPlainText(history["History"])
self.viewLayout.addWidget(self.text)

869
app/ui/setting.py Normal file
View File

@@ -0,0 +1,869 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA设置界面
v4.2
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import (
QWidget,
QApplication,
QVBoxLayout,
QVBoxLayout,
)
from qfluentwidgets import (
ScrollArea,
FluentIcon,
MessageBox,
Dialog,
HyperlinkCard,
HeaderCardWidget,
SwitchSettingCard,
ExpandGroupSettingCard,
PushSettingCard,
ComboBoxSettingCard,
)
from datetime import datetime
import json
import subprocess
import time
import requests
from app.core import Config, MainInfoBar
from app.services import Crypto, System
from app.utils import Updater
from .Widget import LineEditMessageBox, LineEditSettingCard, PasswordLineEditSettingCard
class Setting(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("设置")
content_widget = QWidget()
content_layout = QVBoxLayout(content_widget)
self.function = FunctionSettingCard(self)
self.start = StartSettingCard(self)
self.ui = UiSettingCard(self)
self.notification = NotifySettingCard(self)
self.security = SecuritySettingCard(self)
self.updater = UpdaterSettingCard(self)
self.other = OtherSettingCard(self)
self.function.card_IfAllowSleep.checkedChanged.connect(System.set_Sleep)
self.function.card_IfAgreeBilibili.checkedChanged.connect(self.agree_bilibili)
self.start.card_IfSelfStart.checkedChanged.connect(System.set_SelfStart)
self.security.card_changePASSWORD.clicked.connect(self.change_PASSWORD)
self.updater.card_CheckUpdate.clicked.connect(self.get_update)
self.other.card_Notice.clicked.connect(self.show_notice)
content_layout.addWidget(self.function)
content_layout.addWidget(self.start)
content_layout.addWidget(self.ui)
content_layout.addWidget(self.notification)
content_layout.addWidget(self.security)
content_layout.addWidget(self.updater)
content_layout.addWidget(self.other)
scrollArea = ScrollArea()
scrollArea.setWidgetResizable(True)
scrollArea.setWidget(content_widget)
layout = QVBoxLayout()
layout.addWidget(scrollArea)
self.setLayout(layout)
def agree_bilibili(self) -> None:
"""授权bilibili游戏隐私政策"""
if not Config.global_config.get(Config.global_config.function_IfAgreeBilibili):
logger.info("取消授权bilibili游戏隐私政策")
MainInfoBar.push_info_bar(
"info", "操作成功", "已取消授权bilibili游戏隐私政策", 3000
)
return None
choice = MessageBox(
"授权声明",
"开启“托管bilibili游戏隐私政策”功能即代表您已完整阅读并同意《哔哩哔哩弹幕网用户使用协议》、《哔哩哔哩隐私政策》和《哔哩哔哩游戏中心用户协议》并授权AUTO_MAA在其认定需要时以其认定合适的方法替您处理相关弹窗\n\n是否同意授权?",
self.window(),
)
if choice.exec():
logger.success("确认授权bilibili游戏隐私政策")
MainInfoBar.push_info_bar(
"success", "操作成功", "已确认授权bilibili游戏隐私政策", 3000
)
else:
Config.global_config.set(
Config.global_config.function_IfAgreeBilibili, False
)
def check_PASSWORD(self) -> None:
"""检查并配置管理密钥"""
if Config.key_path.exists():
return None
while True:
choice = LineEditMessageBox(
self.window(),
"未检测到管理密钥,请设置您的管理密钥",
"管理密钥",
"密码",
)
if choice.exec() and choice.input.text() != "":
Crypto.get_PASSWORD(choice.input.text())
break
else:
choice = MessageBox(
"警告",
"您没有设置管理密钥,无法使用本软件,请先设置管理密钥",
self.window(),
)
choice.cancelButton.hide()
choice.buttonLayout.insertStretch(1)
choice.exec()
def change_PASSWORD(self) -> None:
"""修改管理密钥"""
if_change = True
while if_change:
choice = LineEditMessageBox(
self.window(),
"请输入旧的管理密钥",
"旧管理密钥",
"密码",
)
if choice.exec() and choice.input.text() != "":
# 验证旧管理密钥
if Crypto.check_PASSWORD(choice.input.text()):
PASSWORD_old = choice.input.text()
# 获取新的管理密钥
while True:
choice = LineEditMessageBox(
self.window(),
"请输入新的管理密钥",
"新管理密钥",
"密码",
)
if choice.exec() and choice.input.text() != "":
# 修改管理密钥
Crypto.change_PASSWORD(PASSWORD_old, choice.input.text())
MainInfoBar.push_info_bar(
"success", "操作成功", "管理密钥修改成功", 3000
)
if_change = False
break
else:
choice = MessageBox(
"确认",
"您没有输入新的管理密钥,是否取消修改管理密钥?",
self.window(),
)
if choice.exec():
if_change = False
break
else:
choice = MessageBox("错误", "管理密钥错误", self.window())
choice.cancelButton.hide()
choice.buttonLayout.insertStretch(1)
choice.exec()
else:
choice = MessageBox(
"确认",
"您没有输入管理密钥,是否取消修改管理密钥?",
self.window(),
)
if choice.exec():
break
def get_update_info(self) -> str:
"""检查主程序版本更新,返回更新信息"""
# 从本地版本信息文件获取当前版本信息
with Config.version_path.open(mode="r", encoding="utf-8") as f:
version_current = json.load(f)
main_version_current = list(
map(int, version_current["main_version"].split("."))
)
# 从远程服务器获取最新版本信息
for _ in range(3):
try:
response = requests.get(
f"https://gitee.com/DLmaster_361/AUTO_MAA/raw/{Config.global_config.get(Config.global_config.update_UpdateType)}/resources/version.json"
)
version_remote = response.json()
break
except Exception as e:
err = e
time.sleep(0.1)
else:
return f"获取版本信息时出错:\n{err}"
main_version_remote = list(map(int, version_remote["main_version"].split(".")))
# 有版本更新
if main_version_remote > main_version_current:
main_version_info = f" 主程序:{version_text(main_version_current)} --> {version_text(main_version_remote)}\n"
return f"发现新版本:\n{main_version_info} 更新说明:\n{version_remote['announcement'].replace("\n# ","\n ").replace("\n## ","\n - ").replace("\n- ","\n · ")}\n\n是否开始更新?\n\n 注意主程序更新时AUTO_MAA将自动关闭"
else:
return "已是最新版本~"
def get_update(self, if_question: bool = True) -> None:
"""检查版本更新,调起文件下载进程"""
# 从本地版本信息文件获取当前版本信息
with Config.version_path.open(mode="r", encoding="utf-8") as f:
version_current = json.load(f)
main_version_current = list(
map(int, version_current["main_version"].split("."))
)
updater_version_current = list(
map(int, version_current["updater_version"].split("."))
)
# 检查更新器是否存在
if not (Config.app_path / "Updater.exe").exists():
updater_version_current = [0, 0, 0, 0]
# 从远程服务器获取最新版本信息
for _ in range(3):
try:
response = requests.get(
f"https://gitee.com/DLmaster_361/AUTO_MAA/raw/{Config.global_config.get(Config.global_config.update_UpdateType)}/resources/version.json"
)
version_remote = response.json()
break
except Exception as e:
err = e
time.sleep(0.1)
else:
choice = MessageBox(
"错误",
f"获取版本信息时出错:\n{err}",
self.window(),
)
choice.cancelButton.hide()
choice.buttonLayout.insertStretch(1)
if choice.exec():
return None
main_version_remote = list(map(int, version_remote["main_version"].split(".")))
updater_version_remote = list(
map(int, version_remote["updater_version"].split("."))
)
# 有版本更新
if (main_version_remote > main_version_current) or (
updater_version_remote > updater_version_current
):
# 生成版本更新信息
if main_version_remote > main_version_current:
main_version_info = f" 主程序:{version_text(main_version_current)} --> {version_text(main_version_remote)}\n"
else:
main_version_info = (
f" 主程序:{version_text(main_version_current)}\n"
)
if updater_version_remote > updater_version_current:
updater_version_info = f" 更新器:{version_text(updater_version_current)} --> {version_text(updater_version_remote)}\n"
else:
updater_version_info = (
f" 更新器:{version_text(updater_version_current)}\n"
)
# 询问是否开始版本更新
if if_question:
choice = MessageBox(
"版本更新",
f"发现新版本:\n{main_version_info}{updater_version_info} 更新说明:\n{version_remote['announcement'].replace("\n# ","\n ").replace("\n## ","\n - ").replace("\n- ","\n · ")}\n\n是否开始更新?\n\n 注意主程序更新时AUTO_MAA将自动关闭",
self.window(),
)
if not choice.exec():
return None
# 更新更新器
if updater_version_remote > updater_version_current:
# 创建更新进程
self.updater = Updater(
Config.app_path,
"AUTO_MAA更新器",
main_version_remote,
updater_version_remote,
)
# 完成更新器的更新后更新主程序
if main_version_remote > main_version_current:
self.updater.update_process.accomplish.connect(self.update_main)
# 显示更新页面
self.updater.show()
# 更新主程序
elif main_version_remote > main_version_current:
self.update_main()
# 无版本更新
else:
MainInfoBar.push_info_bar("success", "更新检查", "已是最新版本~", 3000)
def update_main(self) -> None:
"""更新主程序"""
subprocess.Popen(
str(Config.app_path / "Updater.exe"),
shell=True,
creationflags=subprocess.CREATE_NO_WINDOW,
)
self.close()
QApplication.quit()
def show_notice(self, if_show: bool = True):
"""显示公告"""
# 从远程服务器获取最新公告
for _ in range(3):
try:
response = requests.get(
"https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/notice.json"
)
notice = response.json()
break
except Exception as e:
err = e
time.sleep(0.1)
else:
logger.warning(f"获取最新公告时出错:\n{err}")
if if_show:
choice = Dialog(
"网络错误",
f"获取最新公告时出错:\n{err}",
self,
)
choice.cancelButton.hide()
choice.buttonLayout.insertStretch(1)
choice.exec()
return None
if (Config.app_path / "resources/notice.json").exists():
with (Config.app_path / "resources/notice.json").open(
mode="r", encoding="utf-8"
) as f:
notice_local = json.load(f)
time_local = datetime.strptime(notice_local["time"], "%Y-%m-%d %H:%M")
else:
time_local = datetime.strptime("2000-01-01 00:00", "%Y-%m-%d %H:%M")
if if_show or (
datetime.now() > datetime.strptime(notice["time"], "%Y-%m-%d %H:%M")
and datetime.strptime(notice["time"], "%Y-%m-%d %H:%M") > time_local
):
choice = Dialog("公告", notice["content"], self)
choice.cancelButton.hide()
choice.buttonLayout.insertStretch(1)
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)
class FunctionSettingCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("功能")
self.card_HomeImageMode = ComboBoxSettingCard(
configItem=Config.global_config.function_HomeImageMode,
icon=FluentIcon.PAGE_RIGHT,
title="主页背景图模式",
content="选择主页背景图的来源",
texts=["默认", "自定义", "主题图像"],
)
self.card_HistoryRetentionTime = ComboBoxSettingCard(
configItem=Config.global_config.function_HistoryRetentionTime,
icon=FluentIcon.PAGE_RIGHT,
title="历史记录保留时间",
content="选择历史记录的保留时间,超期自动清理",
texts=["7 天", "15 天", "30 天", "60 天", "永久"],
)
self.card_IfAllowSleep = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="启动时阻止系统休眠",
content="仅阻止电脑自动休眠,不会影响屏幕是否熄灭",
configItem=Config.global_config.function_IfAllowSleep,
)
self.card_IfSilence = self.SilenceSettingCard(self)
self.card_IfAgreeBilibili = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="托管bilibili游戏隐私政策",
content="授权AUTO_MAA同意bilibili游戏隐私政策",
configItem=Config.global_config.function_IfAgreeBilibili,
)
Layout = QVBoxLayout()
Layout.addWidget(self.card_HomeImageMode)
Layout.addWidget(self.card_HistoryRetentionTime)
Layout.addWidget(self.card_IfAllowSleep)
Layout.addWidget(self.card_IfSilence)
Layout.addWidget(self.card_IfAgreeBilibili)
self.viewLayout.addLayout(Layout)
class SilenceSettingCard(ExpandGroupSettingCard):
def __init__(self, parent=None):
super().__init__(
FluentIcon.SETTING,
"静默模式",
"将各代理窗口置于后台运行,减少对前台的干扰",
parent,
)
self.card_IfSilence = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="静默模式",
content="是否启用静默模式",
configItem=Config.global_config.function_IfSilence,
)
self.card_BossKey = LineEditSettingCard(
text="请输入安卓模拟器老板键",
icon=FluentIcon.PAGE_RIGHT,
title="模拟器老板键",
content="输入模拟器老板快捷键,以“+”分隔",
configItem=Config.global_config.function_BossKey,
)
widget = QWidget()
Layout = QVBoxLayout(widget)
Layout.addWidget(self.card_IfSilence)
Layout.addWidget(self.card_BossKey)
self.viewLayout.setContentsMargins(0, 0, 0, 0)
self.viewLayout.setSpacing(0)
self.addGroupWidget(widget)
class StartSettingCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("启动")
self.card_IfSelfStart = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="开机时自动启动",
content="将AUTO_MAA添加到开机启动项",
configItem=Config.global_config.start_IfSelfStart,
)
self.card_IfRunDirectly = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="启动后直接运行主任务",
content="启动AUTO_MAA后自动运行自动代理任务优先级调度队列 1 > 脚本 1",
configItem=Config.global_config.start_IfRunDirectly,
)
self.card_IfMinimizeDirectly = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="启动后直接最小化",
content="启动AUTO_MAA后直接最小化",
configItem=Config.global_config.start_IfMinimizeDirectly,
)
Layout = QVBoxLayout()
Layout.addWidget(self.card_IfSelfStart)
Layout.addWidget(self.card_IfRunDirectly)
Layout.addWidget(self.card_IfMinimizeDirectly)
self.viewLayout.addLayout(Layout)
class UiSettingCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("界面")
self.card_IfShowTray = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="显示托盘图标",
content="常态显示托盘图标",
configItem=Config.global_config.ui_IfShowTray,
)
self.card_IfToTray = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="最小化到托盘",
content="最小化时隐藏到托盘",
configItem=Config.global_config.ui_IfToTray,
)
Layout = QVBoxLayout()
Layout.addWidget(self.card_IfShowTray)
Layout.addWidget(self.card_IfToTray)
self.viewLayout.addLayout(Layout)
class NotifySettingCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("通知")
self.card_NotifyContent = self.NotifyContentSettingCard(self)
self.card_Plyer = self.PlyerSettingCard(self)
self.card_EMail = self.EMailSettingCard(self)
self.card_ServerChan = self.ServerChanSettingCard(self)
self.card_CompanyWebhookBot = self.CompanyWechatPushSettingCard(self)
Layout = QVBoxLayout()
Layout.addWidget(self.card_NotifyContent)
Layout.addWidget(self.card_Plyer)
Layout.addWidget(self.card_EMail)
Layout.addWidget(self.card_ServerChan)
Layout.addWidget(self.card_CompanyWebhookBot)
self.viewLayout.addLayout(Layout)
class NotifyContentSettingCard(ExpandGroupSettingCard):
def __init__(self, parent=None):
super().__init__(
FluentIcon.SETTING, "通知内容选项", "选择需要推送的通知内容", parent
)
self.card_SendTaskResultTime = ComboBoxSettingCard(
configItem=Config.global_config.notify_SendTaskResultTime,
icon=FluentIcon.PAGE_RIGHT,
title="推送任务结果选项",
content="选择推送自动代理与人工排查任务结果的时机",
texts=["不推送", "任何时刻", "仅失败时"],
)
self.card_IfSendStatistic = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="推送统计信息",
content="推送自动代理统计信息的通知",
configItem=Config.global_config.notify_IfSendStatistic,
)
self.card_IfSendSixStar = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="推送公招高资喜报",
content="公招出现六星词条时推送喜报",
configItem=Config.global_config.notify_IfSendSixStar,
)
widget = QWidget()
Layout = QVBoxLayout(widget)
Layout.addWidget(self.card_SendTaskResultTime)
Layout.addWidget(self.card_IfSendStatistic)
Layout.addWidget(self.card_IfSendSixStar)
self.viewLayout.setContentsMargins(0, 0, 0, 0)
self.viewLayout.setSpacing(0)
self.addGroupWidget(widget)
class PlyerSettingCard(ExpandGroupSettingCard):
def __init__(self, parent=None):
super().__init__(
FluentIcon.SETTING, "推送系统通知", "Plyer系统通知推送渠道", parent
)
self.card_IfPushPlyer = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="推送系统通知",
content="使用Plyer推送系统级通知不会在通知中心停留",
configItem=Config.global_config.notify_IfPushPlyer,
)
widget = QWidget()
Layout = QVBoxLayout(widget)
Layout.addWidget(self.card_IfPushPlyer)
self.viewLayout.setContentsMargins(0, 0, 0, 0)
self.viewLayout.setSpacing(0)
self.addGroupWidget(widget)
class EMailSettingCard(ExpandGroupSettingCard):
def __init__(self, parent=None):
super().__init__(
FluentIcon.SETTING, "推送邮件通知", "电子邮箱通知推送渠道", parent
)
self.card_IfSendMail = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="推送邮件通知",
content="是否启用邮件通知功能",
configItem=Config.global_config.notify_IfSendMail,
)
self.card_SMTPServerAddress = LineEditSettingCard(
text="请输入SMTP服务器地址",
icon=FluentIcon.PAGE_RIGHT,
title="SMTP服务器地址",
content="发信邮箱的SMTP服务器地址",
configItem=Config.global_config.notify_SMTPServerAddress,
)
self.card_FromAddress = LineEditSettingCard(
text="请输入发信邮箱地址",
icon=FluentIcon.PAGE_RIGHT,
title="发信邮箱地址",
content="发送通知的邮箱地址",
configItem=Config.global_config.notify_FromAddress,
)
self.card_AuthorizationCode = PasswordLineEditSettingCard(
text="请输入发信邮箱授权码",
icon=FluentIcon.PAGE_RIGHT,
title="发信邮箱授权码",
content="发送通知的邮箱授权码",
configItem=Config.global_config.notify_AuthorizationCode,
)
self.card_ToAddress = LineEditSettingCard(
text="请输入收信邮箱地址",
icon=FluentIcon.PAGE_RIGHT,
title="收信邮箱地址",
content="接收通知的邮箱地址",
configItem=Config.global_config.notify_ToAddress,
)
widget = QWidget()
Layout = QVBoxLayout(widget)
Layout.addWidget(self.card_IfSendMail)
Layout.addWidget(self.card_SMTPServerAddress)
Layout.addWidget(self.card_FromAddress)
Layout.addWidget(self.card_AuthorizationCode)
Layout.addWidget(self.card_ToAddress)
self.viewLayout.setContentsMargins(0, 0, 0, 0)
self.viewLayout.setSpacing(0)
self.addGroupWidget(widget)
class ServerChanSettingCard(ExpandGroupSettingCard):
def __init__(self, parent=None):
super().__init__(
FluentIcon.SETTING,
"推送ServerChan通知",
"ServerChan通知推送渠道",
parent,
)
self.card_IfServerChan = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="推送SeverChan通知",
content="是否启用SeverChan通知功能",
configItem=Config.global_config.notify_IfServerChan,
)
self.card_ServerChanKey = LineEditSettingCard(
text="请输入SendKey",
icon=FluentIcon.PAGE_RIGHT,
title="SendKey",
content="Server酱的SendKeySC3与SCT都可以",
configItem=Config.global_config.notify_ServerChanKey,
)
self.card_ServerChanChannel = LineEditSettingCard(
text="请输入需要推送的Channel代码SCT生效",
icon=FluentIcon.PAGE_RIGHT,
title="ServerChanChannel代码",
content="可以留空,留空则默认。可以多个,请使用“|”隔开",
configItem=Config.global_config.notify_ServerChanChannel,
)
self.card_ServerChanTag = LineEditSettingCard(
text="请输入加入推送的TagSC3生效",
icon=FluentIcon.PAGE_RIGHT,
title="Tag内容",
content="可以留空,留空则默认。可以多个,请使用“|”隔开",
configItem=Config.global_config.notify_ServerChanTag,
)
widget = QWidget()
Layout = QVBoxLayout(widget)
Layout.addWidget(self.card_IfServerChan)
Layout.addWidget(self.card_ServerChanKey)
Layout.addWidget(self.card_ServerChanChannel)
Layout.addWidget(self.card_ServerChanTag)
self.viewLayout.setContentsMargins(0, 0, 0, 0)
self.viewLayout.setSpacing(0)
self.addGroupWidget(widget)
class CompanyWechatPushSettingCard(ExpandGroupSettingCard):
def __init__(self, parent=None):
super().__init__(
FluentIcon.SETTING,
"推送企业微信机器人通知",
"企业微信机器人Webhook通知推送渠道",
parent,
)
self.card_IfCompanyWechat = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="推送企业微信机器人通知",
content="是否启用企业微信机器人通知功能",
configItem=Config.global_config.notify_IfCompanyWebHookBot,
)
self.card_CompanyWebHookBotUrl = LineEditSettingCard(
text="请输入Webhook的Url",
icon=FluentIcon.PAGE_RIGHT,
title="WebhookUrl",
content="企业微信群机器人的Webhook地址",
configItem=Config.global_config.notify_CompanyWebHookBotUrl,
)
widget = QWidget()
Layout = QVBoxLayout(widget)
Layout.addWidget(self.card_IfCompanyWechat)
Layout.addWidget(self.card_CompanyWebHookBotUrl)
self.viewLayout.setContentsMargins(0, 0, 0, 0)
self.viewLayout.setSpacing(0)
self.addGroupWidget(widget)
class SecuritySettingCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("安全")
self.card_changePASSWORD = PushSettingCard(
text="修改",
icon=FluentIcon.VPN,
title="修改管理密钥",
content="修改用于解密用户密码的管理密钥",
)
Layout = QVBoxLayout()
Layout.addWidget(self.card_changePASSWORD)
self.viewLayout.addLayout(Layout)
class UpdaterSettingCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("更新")
self.card_IfAutoUpdate = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="自动检查更新",
content="将在启动时自动检查AUTO_MAA是否有新版本",
configItem=Config.global_config.update_IfAutoUpdate,
)
self.card_UpdateType = ComboBoxSettingCard(
configItem=Config.global_config.update_UpdateType,
icon=FluentIcon.PAGE_RIGHT,
title="版本更新类别",
content="选择AUTO_MAA的更新类别",
texts=["稳定版", "公测版"],
)
self.card_CheckUpdate = PushSettingCard(
text="检查更新",
icon=FluentIcon.UPDATE,
title="获取最新版本",
content="检查AUTO_MAA是否有新版本",
)
Layout = QVBoxLayout()
Layout.addWidget(self.card_IfAutoUpdate)
Layout.addWidget(self.card_UpdateType)
Layout.addWidget(self.card_CheckUpdate)
self.viewLayout.addLayout(Layout)
class OtherSettingCard(HeaderCardWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle("其他")
self.card_Notice = PushSettingCard(
text="查看",
icon=FluentIcon.PAGE_RIGHT,
title="公告",
content="查看AUTO_MAA的最新公告",
)
self.card_UserDocs = HyperlinkCard(
url="https://clozya.github.io/AUTOMAA_docs",
text="访问",
icon=FluentIcon.PAGE_RIGHT,
title="AUTO_MAA官方文档站",
content="访问AUTO_MAA的官方文档站获取使用指南和项目相关信息",
)
self.card_Association = self.AssociationSettingCard()
Layout = QVBoxLayout()
Layout.addWidget(self.card_Notice)
Layout.addWidget(self.card_UserDocs)
Layout.addWidget(self.card_Association)
self.viewLayout.addLayout(Layout)
class AssociationSettingCard(ExpandGroupSettingCard):
def __init__(self, parent=None):
super().__init__(
FluentIcon.SETTING,
"AUTO_MAA官方社群",
"加入AUTO_MAA官方社群获取更多帮助",
parent,
)
self.card_GitHubRepository = HyperlinkCard(
url="https://github.com/DLmaster361/AUTO_MAA",
text="访问GitHub仓库",
icon=FluentIcon.GITHUB,
title="GitHub",
content="查看AUTO_MAA的源代码提交问题和建议欢迎参与开发",
)
self.card_QQGroup = HyperlinkCard(
url="https://qm.qq.com/q/bd9fISNoME",
text="加入官方QQ交流群",
icon=FluentIcon.CHAT,
title="QQ群",
content="与AUTO_MAA开发者和用户交流",
)
widget = QWidget()
Layout = QVBoxLayout(widget)
Layout.addWidget(self.card_GitHubRepository)
Layout.addWidget(self.card_QQGroup)
self.viewLayout.setContentsMargins(0, 0, 0, 0)
self.viewLayout.setSpacing(0)
self.addGroupWidget(widget)
def version_text(version_numb: list) -> str:
"""将版本号列表转为可读的文本信息"""
if version_numb[3] == 0:
version = f"v{'.'.join(str(_) for _ in version_numb[0:3])}"
else:
version = (
f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}"
)
return version

490
app/utils/Updater.py Normal file
View File

@@ -0,0 +1,490 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA更新器
v1.1
作者DLmaster_361
"""
import sys
import json
import zipfile
import requests
import subprocess
import time
from pathlib import Path
from PySide6.QtWidgets import QApplication, QDialog, QVBoxLayout, QHBoxLayout
from qfluentwidgets import (
ProgressBar,
IndeterminateProgressBar,
BodyLabel,
PushButton,
EditableComboBox,
)
from PySide6.QtGui import QIcon, QCloseEvent
from PySide6.QtCore import QThread, Signal, QEventLoop
def version_text(version_numb: list) -> str:
"""将版本号列表转为可读的文本信息"""
if version_numb[3] == 0:
version = f"v{'.'.join(str(_) for _ in version_numb[0:3])}"
else:
version = (
f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}"
)
return version
class UpdateProcess(QThread):
info = Signal(str)
progress = Signal(int, int, int)
question = Signal(dict)
question_response = Signal(str)
accomplish = Signal()
def __init__(
self, app_path: Path, name: str, main_version: list, updater_version: list
) -> None:
super(UpdateProcess, self).__init__()
self.app_path = app_path
self.name = name
self.main_version = main_version
self.updater_version = updater_version
self.download_path = app_path / "DOWNLOAD_TEMP.zip" # 临时下载文件的路径
self.version_path = app_path / "resources/version.json"
self.response = None
self.question_response.connect(self._capture_response)
def run(self) -> None:
# 清理可能存在的临时文件
if self.download_path.exists():
self.download_path.unlink()
self.info.emit("正在获取下载链接")
url_list = self.get_download_url()
url_dict = {}
# 验证下载地址
for i, url in enumerate(url_list):
if self.isInterruptionRequested():
return None
self.progress.emit(0, len(url_list), i)
try:
self.info.emit(f"正在验证下载地址:{url}")
response = requests.get(url, stream=True)
if response.status_code != 200:
self.info.emit(f"连接失败,错误代码 {response.status_code}")
time.sleep(1)
continue
url_dict[url] = response.elapsed.total_seconds()
except requests.RequestException:
self.info.emit(f"请求超时")
time.sleep(1)
download_url = self.push_question(url_dict)
# 获取文件大小
try:
self.info.emit(f"正在连接下载地址:{download_url}")
self.progress.emit(0, 0, 0)
response = requests.get(download_url, stream=True)
if response.status_code != 200:
self.info.emit(f"连接失败,错误代码 {response.status_code}")
return None
file_size = response.headers.get("Content-Length")
except requests.RequestException:
self.info.emit(f"请求超时")
return None
if file_size is None:
file_size = 1
else:
file_size = int(file_size)
try:
# 下载文件
with open(self.download_path, "wb") as f:
downloaded_size = 0
last_download_size = 0
speed = 0
last_time = time.time()
for chunk in response.iter_content(chunk_size=8192):
if self.isInterruptionRequested():
break
# 写入已下载数据
f.write(chunk)
downloaded_size += len(chunk)
# 计算下载速度
if time.time() - last_time >= 1.0:
speed = (
(downloaded_size - last_download_size)
/ (time.time() - last_time)
/ 1024
)
last_download_size = downloaded_size
last_time = time.time()
# 更新下载进度
if speed >= 1024:
self.info.emit(
f"正在下载:{self.name} 已下载:{downloaded_size / 1048576:.2f}/{file_size / 1048576:.2f} MB {downloaded_size / file_size * 100:.2f}% 下载速度:{speed / 1024:.2f} MB/s",
)
else:
self.info.emit(
f"正在下载:{self.name} 已下载:{downloaded_size / 1048576:.2f}/{file_size / 1048576:.2f} MB {downloaded_size / file_size * 100:.2f}% 下载速度:{speed:.2f} KB/s",
)
self.progress.emit(0, 100, int(downloaded_size / file_size * 100))
if self.isInterruptionRequested() and self.download_path.exists():
self.download_path.unlink()
return None
except Exception as e:
e = str(e)
e = "\n".join([e[_ : _ + 75] for _ in range(0, len(e), 75)])
self.info.emit(f"下载{self.name}时出错:\n{e}")
return None
# 解压
try:
while True:
if self.isInterruptionRequested():
self.download_path.unlink()
return None
try:
self.info.emit("正在解压更新文件")
self.progress.emit(0, 0, 0)
with zipfile.ZipFile(self.download_path, "r") as zip_ref:
zip_ref.extractall(self.app_path)
break
except PermissionError:
self.info.emit(f"解压出错:{self.name}正在运行,正在等待其关闭")
time.sleep(1)
self.info.emit("正在删除临时文件")
self.progress.emit(0, 0, 0)
self.download_path.unlink()
self.info.emit(f"{self.name}更新成功!")
self.progress.emit(0, 100, 100)
except Exception as e:
e = str(e)
e = "\n".join([e[_ : _ + 75] for _ in range(0, len(e), 75)])
self.info.emit(f"解压更新时出错:\n{e}")
return None
# 更新version文件
if not self.isInterruptionRequested and self.name in [
"AUTO_MAA主程序",
"AUTO_MAA更新器",
]:
with open(self.version_path, "r", encoding="utf-8") as f:
version_info = json.load(f)
if self.name == "AUTO_MAA主程序":
version_info["main_version"] = ".".join(map(str, self.main_version))
elif self.name == "AUTO_MAA更新器":
version_info["updater_version"] = ".".join(
map(str, self.updater_version)
)
with open(self.version_path, "w", encoding="utf-8") as f:
json.dump(version_info, f, ensure_ascii=False, indent=4)
# 主程序更新完成后打开AUTO_MAA
if not self.isInterruptionRequested and self.name == "AUTO_MAA主程序":
subprocess.Popen(
str(self.app_path / "AUTO_MAA.exe"),
shell=True,
creationflags=subprocess.CREATE_NO_WINDOW,
)
elif not self.isInterruptionRequested and self.name == "MAA":
subprocess.Popen(
str(self.app_path / "MAA.exe"),
shell=True,
creationflags=subprocess.CREATE_NO_WINDOW,
)
self.accomplish.emit()
def get_download_url(self) -> list:
"""获取下载链接"""
try_num = 3
for i in range(try_num):
try:
response = requests.get(
"https://gitee.com/DLmaster_361/AUTO_MAA/raw/main/resources/version.json"
)
if response.status_code != 200:
self.info.emit(
f"连接失败,错误代码 {response.status_code} ,正在重试({i+1}/{try_num}"
)
time.sleep(0.1)
continue
version_remote = response.json()
PROXY_list = version_remote["proxy_list"]
break
except requests.RequestException:
self.info.emit(f"请求超时,正在重试({i+1}/{try_num}")
time.sleep(0.1)
except KeyError:
self.info.emit(f"未找到远端代理网址项,正在重试({i+1}/{try_num}")
time.sleep(0.1)
else:
self.info.emit("获取远端代理信息失败,将使用默认代理地址")
PROXY_list = [
"",
"https://gitproxy.click/",
"https://cdn.moran233.xyz/",
"https://gh.llkk.cc/",
"https://github.akams.cn/",
"https://www.ghproxy.cn/",
"https://ghfast.top/",
]
time.sleep(1)
url_list = []
if self.name == "AUTO_MAA主程序":
url_list.append(
f"https://gitee.com/DLmaster_361/AUTO_MAA/releases/download/{version_text(self.main_version)}/AUTO_MAA_{version_text(self.main_version)}.zip"
)
url_list.append(
f"https://jp-download.fearr.xyz/AUTO_MAA/AUTO_MAA_{version_text(self.main_version)}.zip"
)
for i in range(len(PROXY_list)):
url_list.append(
f"{PROXY_list[i]}https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.main_version)}/AUTO_MAA_{version_text(self.main_version)}.zip"
)
elif self.name == "AUTO_MAA更新器":
url_list.append(
f"https://gitee.com/DLmaster_361/AUTO_MAA/releases/download/{version_text(self.main_version)}/Updater_{version_text(self.updater_version)}.zip"
)
url_list.append(
f"https://jp-download.fearr.xyz/AUTO_MAA/Updater_{version_text(self.updater_version)}.zip"
)
for i in range(len(PROXY_list)):
url_list.append(
f"{PROXY_list[i]}https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.main_version)}/Updater_{version_text(self.updater_version)}.zip"
)
elif self.name == "MAA":
url_list.append(
f"https://jp-download.fearr.xyz/MAA/MAA-{version_text(self.main_version)}-win-x64.zip"
)
for i in range(len(PROXY_list)):
url_list.append(
f"{PROXY_list[i]}https://github.com/MaaAssistantArknights/MaaAssistantArknights/releases/download/{version_text(self.main_version)}/MAA-{version_text(self.main_version)}-win-x64.zip"
)
return url_list
def push_question(self, url_dict: dict) -> str:
self.question.emit(url_dict)
loop = QEventLoop()
self.question_response.connect(loop.quit)
loop.exec()
return self.response
def _capture_response(self, response: str) -> None:
self.response = response
class Updater(QDialog):
def __init__(
self, app_path: Path, name: str, main_version: list, updater_version: list
) -> None:
super().__init__()
self.setWindowTitle("AUTO_MAA更新器")
self.setWindowIcon(
QIcon(
str(
Path(sys.argv[0]).resolve().parent
/ "resources/icons/AUTO_MAA_Updater.ico"
)
)
)
# 创建垂直布局
self.Layout = QVBoxLayout(self)
self.info = BodyLabel("正在初始化", self)
self.progress_1 = IndeterminateProgressBar(self)
self.progress_2 = ProgressBar(self)
self.combo_box = EditableComboBox(self)
self.button = PushButton("继续", self)
self.h_layout = QHBoxLayout()
self.h_layout.addStretch(1)
self.h_layout.addWidget(self.button)
self.update_progress(0, 0, 0)
self.Layout.addWidget(self.info)
self.Layout.addStretch(1)
self.Layout.addWidget(self.progress_1)
self.Layout.addWidget(self.progress_2)
self.Layout.addWidget(self.combo_box)
self.Layout.addLayout(self.h_layout)
self.Layout.addStretch(1)
self.update_process = UpdateProcess(
app_path, name, main_version, updater_version
)
self.update_process.info.connect(self.update_info)
self.update_process.progress.connect(self.update_progress)
self.update_process.question.connect(self.question)
self.update_process.start()
def update_info(self, text: str) -> None:
self.info.setText(text)
def update_progress(
self, begin: int, end: int, current: int, if_show_combo_box: bool = False
) -> None:
self.combo_box.setVisible(if_show_combo_box)
self.button.setVisible(if_show_combo_box)
if if_show_combo_box:
self.progress_1.setVisible(False)
self.progress_2.setVisible(False)
self.resize(1000, 90)
elif begin == 0 and end == 0:
self.progress_2.setVisible(False)
self.progress_1.setVisible(True)
self.resize(700, 70)
else:
self.progress_1.setVisible(False)
self.progress_2.setVisible(True)
self.progress_2.setRange(begin, end)
self.progress_2.setValue(current)
self.resize(700, 70)
def question(self, url_dict: dict) -> None:
self.update_info("测速完成,请选择或自行输入一个合适下载地址:")
self.update_progress(0, 0, 0, True)
url_dict = dict(sorted(url_dict.items(), key=lambda item: item[1]))
for url, time in url_dict.items():
self.combo_box.addItem(f"{url} | 响应时间:{time:.3f}")
self.button.clicked.connect(
lambda: self.update_process.question_response.emit(
self.combo_box.currentText().split(" | ")[0]
)
)
def closeEvent(self, event: QCloseEvent):
"""清理残余进程"""
self.update_process.requestInterruption()
self.update_process.quit()
self.update_process.wait()
event.accept()
class AUTO_MAA_Updater(QApplication):
def __init__(
self, app_path: Path, name: str, main_version: list, updater_version: list
) -> None:
super().__init__()
self.main = Updater(app_path, name, main_version, updater_version)
self.main.show()
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:
version_current = json.load(f)
main_version_current = list(
map(int, version_current["main_version"].split("."))
)
else:
main_version_current = [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 and "UpdateType" in config["Update"]:
update_type = config["Update"]["UpdateType"]
else:
update_type = "main"
else:
update_type = "main"
# 从远程服务器获取最新版本信息
for _ in range(3):
try:
response = requests.get(
f"https://gitee.com/DLmaster_361/AUTO_MAA/raw/{update_type}/resources/version.json"
)
version_remote = response.json()
main_version_remote = list(
map(int, version_remote["main_version"].split("."))
)
break
except Exception as e:
err = e
time.sleep(0.1)
else:
sys.exit(f"获取版本信息时出错:\n{err}")
# 启动更新线程
if main_version_remote > main_version_current:
app = AUTO_MAA_Updater(
app_path,
"AUTO_MAA主程序",
main_version_remote,
[],
)
sys.exit(app.exec())

34
app/utils/__init__.py Normal file
View File

@@ -0,0 +1,34 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA工具包
v4.2
作者DLmaster_361
"""
__version__ = "4.2.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .Updater import Updater
__all__ = ["Updater"]

109
app/utils/package.py Normal file
View File

@@ -0,0 +1,109 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA打包程序
v4.2
作者DLmaster_361
"""
import os
import sys
import json
import shutil
from pathlib import Path
def version_text(version_numb: list) -> str:
"""将版本号列表转为可读的文本信息"""
if version_numb[3] == 0:
version = f"v{'.'.join(str(_) for _ in version_numb[0:3])}"
else:
version = (
f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}"
)
return version
if __name__ == "__main__":
root_path = Path(sys.argv[0]).resolve().parent
with (root_path / "resources/version.json").open(mode="r", encoding="utf-8") as f:
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"
" --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"]}"
" --file-description='AUTO_MAA Component'"
" --copyright='Copyright © 2024 DLmaster361'"
" --assume-yes-for-downloads --output-filename=AUTO_MAA"
" --remove-output main.py"
)
print("AUTO_MAA main program packaging completed !")
shutil.copy(root_path / "app/utils/Updater.py", root_path)
file_content = (root_path / "Updater.py").read_text(encoding="utf-8")
(root_path / "Updater.py").write_text(
file_content.replace(
"from .version import version_text", "from app import version_text"
),
encoding="utf-8",
)
print("Packaging AUTO_MAA update program ...")
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 DLmaster361'"
" --assume-yes-for-downloads --output-filename=Updater"
" --remove-output Updater.py"
)
print("AUTO_MAA update program packaging completed !")
(root_path / "Updater.py").unlink()
(root_path / "version_info.txt").write_text(
f"{version_text(main_version_numb)}\n{version_text(updater_version_numb)}{version["announcement"]}",
encoding="utf-8",
)

View File

@@ -1,5 +0,0 @@
龙门币CE-6
技能CA-5
红票AP-5
经验CA-5
剿灭模式Annihilation

54
main.py Normal file
View File

@@ -0,0 +1,54 @@
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
# Copyright © <2024> <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/>.
# DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA主程序
v4.2
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import Qt
from qfluentwidgets import FluentTranslator
import sys
@logger.catch
def main():
application = QApplication(sys.argv)
QApplication.setAttribute(Qt.AA_DontCreateNativeWidgetSiblings)
translator = FluentTranslator()
application.installTranslator(translator)
from app.ui.main_window import AUTO_MAA
window = AUTO_MAA()
window.show_ui("显示主窗口")
window.start_up_task()
sys.exit(application.exec())
if __name__ == "__main__":
main()

238
manage.py
View File

@@ -1,238 +0,0 @@
import sqlite3
import datetime
import os
#添加用户
def add():
db=sqlite3.connect(DATABASE)
cur=db.cursor()
adminx=input("用户名:")
#用户名重复验证
while search(adminx,0)=="":
print("该用户已存在,请重新输入")
adminx=input("用户名:")
numberx=input("手机号码:")
dayx=int(input("代理天数:"))
gamex=input("关卡号:")
passwordx=input("密码:")
#应用更新
cur.execute("INSERT INTO adminx(admin,number,day,status,last,game,password) VALUES('%s','%s',%d,'y','2000-01-01','%s','%s')" %(adminx,numberx,dayx,gamex,passwordx))
db.commit()
cur.close()
db.close()
return "操作成功"
#删除用户信息
def delete(id):
db=sqlite3.connect(DATABASE)
cur=db.cursor()
#检查用户是否存在
cur.execute("SELECT * FROM adminx WHERE admin='%s'" %(id))
data=cur.fetchall()
if len(data)==0:
return "未找到"+id
#应用更新
cur.execute("DELETE FROM adminx WHERE admin='%s'" %(id))
db.commit()
cur.close()
db.close()
return "成功删除"+id
#检索用户信息与配置
def search(id,book):
db=sqlite3.connect(DATABASE)
cur=db.cursor()
#处理MAA路径查询
if id=="maa":
cur.execute("SELECT * FROM setting WHERE True")
pathx=cur.fetchall()
if len(pathx)>0:
return pathx[0][0]
else:
return "MAA路径未设置"
#处理用户查询与全部信息查询
if id=="all":
cur.execute("SELECT * FROM adminx WHERE True")
else:
cur.execute("SELECT * FROM adminx WHERE admin='%s'" %(id))
data=cur.fetchall()
if id=="all":
cur.execute("SELECT * FROM setting WHERE True")
pathx=cur.fetchall()
if len(pathx)>0:
print("\nMAA路径"+pathx[0][0])
else:
print("\nMAA路径未设置")
cur.close()
db.close()
data=[list(row) for row in data]
if len(data)>0:
#转译执行情况、用户状态,对全部信息查询隐去密码
curdate=datetime.date.today()
curdate=curdate.strftime('%Y-%m-%d')
for i in range(len(data)):
if data[i][4]==curdate:
data[i][4]="今日已执行"
else:
data[i][4]="今日未执行"
if data[i][3]=='y':
data[i][3]="启用"
else:
data[i][3]="禁用"
if id=="all":
data[i][6]="******"
#制表输出
if book==1:
print('')
print(unit("用户名",15),unit("手机号码",12),unit("代理天数",8),unit("状态",4),unit("执行情况",10),unit("关卡",10),unit("密码",25))
for i in range(len(data)):
print(unit(data[i][0],15),unit(data[i][1],12),unit(data[i][2],8),unit(data[i][3],4),unit(data[i][4],10),unit(data[i][5],10),unit(data[i][6],25))
return ""
elif id=="all":
return "当前没有用户记录"
else:
return "未找到"+id
#续期
def renewal(readxx):
#提取用户名与续期时间
for i in range(len(readxx)):
if readxx[i]==' ':
id=readxx[:i]
dayp=int(readxx[i+1:])
break
#检查用户是否存在
db=sqlite3.connect(DATABASE)
cur=db.cursor()
cur.execute("SELECT * FROM adminx WHERE admin='%s'" %(id))
data=cur.fetchall()
if len(data)==0:
return "未找到"+id
#应用更新
cur.execute("UPDATE adminx SET day=%d WHERE admin='%s'" %(data[0][2]+dayp,id))
db.commit()
cur.close()
db.close()
return '成功更新'+id+'的代理天数至'+str(data[0][2]+dayp)+''
#用户状态配置
def turn(id,t):
#检查用户是否存在
db=sqlite3.connect(DATABASE)
cur=db.cursor()
cur.execute("SELECT * FROM adminx WHERE admin='%s'" %(id))
data=cur.fetchall()
if len(data)==0:
return "未找到"+id
#应用更新
if t=='y' or t=='n':
cur.execute("UPDATE adminx SET status='%s' WHERE admin='%s'" %(t,id))
db.commit()
cur.close()
db.close()
if t=='y':
return '已启用'+id
else:
return '已禁用'+id
#修改刷取关卡
def gameid(readxx):
#提取用户名与修改值
for i in range(len(readxx)):
if readxx[i]==' ':
id=readxx[:i]
gamep=readxx[i+1:]
break
#检查用户是否存在
db=sqlite3.connect(DATABASE)
cur=db.cursor()
cur.execute("SELECT * FROM adminx WHERE admin='%s'" %(id))
data=cur.fetchall()
if len(data)==0:
return "未找到"+id
#导入与应用特殊关卡规则
games={}
with open('data/gameid.txt',encoding='utf-8') as f:
gameids=f.readlines()
for i in range(len(gameids)):
for j in range(len(gameids[i])):
if gameids[i][j]=='':
gamein=gameids[i][:j]
gameout=gameids[i][j+1:]
break
games[gamein]=gameout.strip()
if gamep in games:
gamep=games[gamep]
#应用更新
cur.execute("UPDATE adminx SET game='%s' WHERE admin='%s'" %(gamep,id))
db.commit()
cur.close()
db.close()
return '成功更新'+id+'的关卡为'+gamep
#设置MAA路径
def setpath(pathx):
db=sqlite3.connect(DATABASE)
cur=db.cursor()
cur.execute("SELECT * FROM setting WHERE True")
pathold=cur.fetchall()
if len(pathold)>0:
cur.execute("UPDATE setting SET path='%s' WHERE True" %(pathx))
else:
cur.execute("INSERT INTO setting(path) VALUES('%s')" %(pathx))
db.commit()
cur.close()
db.close()
return "MAA路径已设置为"+pathx
#统一制表单元
def unit(x,m):
#字母与连接符占1位中文占2位
x=str(x)
n=0
for i in x:
if 'a'<=i<='z' or 'A'<=i<='Z' or '0'<=i<='9' or i=='_' or i=='-':
n+=1
return ' '+x+' '*(m-2*len(x)+n)
#初期检查
DATABASE="data/data.db"
if not os.path.exists(DATABASE):
db=sqlite3.connect(DATABASE)
cur=db.cursor()
db.execute("CREATE TABLE adminx(admin text,number text,day int,status text,last date,game text,password text)")
db.execute("CREATE TABLE setting(path text)")
readx=input("首次启动请设置MAA路径")
cur.execute("INSERT INTO setting(path) VALUES('%s')" %(readx))
db.commit()
cur.close()
db.close()
#初始界面
print("Good evening!")
print(search("all",1))
#主程序
while True:
read=input()
if len(read)==0:
print("无法识别的输入")
elif read[0]=='+':
print(add())
elif read[0]=='-':
exit()
elif read[0]=='/':
print(setpath(read[1:]))
else:
if read[-1]=='?':
print(search(read[:-2],1))
elif read[-1]=='+':
print(renewal(read[:-2]))
elif read[-1]=='-':
print(delete(read[:-2]))
elif read[-1]=='~':
print(gameid(read[:-2]))
elif read[-1]=='y' or read[-1]=='n':
print(turn(read[:-2],read[-1]))
else:
print("无法识别的输入")

Binary file not shown.

13
requirements.txt Normal file
View File

@@ -0,0 +1,13 @@
loguru
plyer
PySide6
PySide6-Fluent-Widgets[full]
psutil
opencv-python
pywin32
pyautogui
pycryptodome
requests
Jinja2
serverchan_sdk
nuitka==2.6

14
res/version.json Normal file
View File

@@ -0,0 +1,14 @@
{
"main_version": "4.2.0.0",
"updater_version": "1.1.0.0",
"announcement": "\n# 这是一个中转版本,此版本后更换程序架构方式。\n# 由于更新方法无法通用,您需要在完成本次更新后再次检查更新以获取最新版本。\n",
"proxy_list":[
"",
"https://gitproxy.click/",
"https://cdn.moran233.xyz/",
"https://gh.llkk.cc/",
"https://github.akams.cn/",
"https://www.ghproxy.cn/",
"https://ghfast.top/"
]
}

View File

@@ -0,0 +1,47 @@
#主界面
"MainFunction.PostActions": "8" #完成后退出MAA
"MainFunction.PostActions": "9" #完成后退出MAA和游戏
"MainFunction.PostActions": "12" #完成后退出MAA和模拟器
"TaskQueue.WakeUp.IsChecked": "True" #开始唤醒
"TaskQueue.Recruiting.IsChecked": "True" #自动公招
"TaskQueue.Base.IsChecked": "True" #基建换班
"TaskQueue.Combat.IsChecked": "True" #刷理智
"TaskQueue.Mission.IsChecked": "True" #领取奖励
"TaskQueue.Mall.IsChecked": "True" #获取信用及购物
"TaskQueue.AutoRoguelike.IsChecked": "False" #自动肉鸽
"TaskQueue.Reclamation.IsChecked": "False" #生息演算
#刷理智
"MainFunction.Stage1": "" #主关卡
"MainFunction.Stage2": "" #备选关卡1
"MainFunction.Stage3": "" #备选关卡2
"Fight.RemainingSanityStage": "Annihilation" #剩余理智关卡
"MainFunction.Series.Quantity": "1" #连战次数
"Penguin.IsDrGrandet": "True" #博朗台模式
"GUI.CustomStageCode": "False" #手动输入关卡名
"GUI.UseAlternateStage": "False" #使用备选关卡
"Fight.UseRemainingSanityStage": "True" #使用剩余理智
"GUI.AllowUseStoneSave": "False" #允许吃源石保持状态
"Fight.UseExpiringMedicine": "False" #无限吃48小时内过期的理智药
"GUI.HideUnavailableStage": "False" #隐藏当日不开放关卡
"GUI.HideSeries": "False" #隐藏连战次数
"Infrast.CustomInfrastPlanShowInFightSettings": "False" #显示基建计划
"Penguin.EnablePenguin": "True" #上报企鹅物流
"Yituliu.EnableYituliu": "True" #上报一图流
#基建换班
"Infrast.CustomInfrastEnabled": "True" #启用自定义基建配置
"Infrast.CustomInfrastPlanIndex": "1" #自定义基建配置索引
"Infrast.DefaultInfrast": "user_defined" #内置配置
"Infrast.IsCustomInfrastFileReadOnly": "False" #自定义基建配置文件只读
"Infrast.CustomInfrastFile": "" #自定义基建配置文件地址
#设置
"Start.ClientType": "Bilibili"、 "Official" #服务器
G"Timer.Timer1": "False" #时间设置1
G"VersionUpdate.ScheduledUpdateCheck": "True" #定时检查更新
G"VersionUpdate.AutoDownloadUpdatePackage": "True" #自动下载更新包
G"VersionUpdate.AutoInstallUpdatePackage": "True" #自动安装更新包
G"Start.MinimizeDirectly": "True" #启动MAA后直接最小化
"Start.RunDirectly": "True" #启动MAA后直接运行
"Start.OpenEmulatorAfterLaunch": "True" #启动MAA后自动开启模拟器
G"GUI.UseTray": "True" #显示托盘图标
G"GUI.MinimizeToTray": "False" #最小化时隐藏至托盘
"Start.EmulatorPath" #模拟器路径

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
resources/images/Home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

14
resources/version.json Normal file
View File

@@ -0,0 +1,14 @@
{
"main_version": "4.2.5.1",
"updater_version": "1.1.2.1",
"announcement": "\n## 新增功能\n- 暂无\n## 修复BUG\n- 修复统计信息HTML模板公招匹配错误\n## 程序优化\n- 暂无",
"proxy_list": [
"",
"https://gitproxy.click/",
"https://cdn.moran233.xyz/",
"https://gh.llkk.cc/",
"https://github.akams.cn/",
"https://www.ghproxy.cn/",
"https://ghfast.top/"
]
}

112
run.py
View File

@@ -1,112 +0,0 @@
import os
import sqlite3
import datetime
import time
import json
from termcolor import colored
def ifoff():
while True:
time.sleep(10)
with open(logpath,'r',encoding='utf-8') as f:
logs=f.readlines()[-1:-10:-1]
log=''.join(logs)
print(colored('\n'.join(logs[::-1]),"green"))
if "任务已全部完成!" in log:
return True
elif ("请检查连接设置或尝试重启模拟器与 ADB 或重启电脑" in log) or ("已停止" in log):
return False
def killpath(maapath):
kpath='.\\mytaskkill.exe '
for i in maapath:
if i=='/':
kpath=kpath+'\\'
else:
kpath=kpath+i
return kpath
def runmaa(tel,game):
with open(setpath,"r",encoding="utf-8") as f:
data = json.load(f)
data["Configurations"]["Default"]["Start.AccountName"]=tel[:3]+"****"+tel[7:]
week=str(datetime.datetime.now().strftime('%A'))
if week=="Monday":
data["Configurations"]["Default"]["MainFunction.Stage1"]="Annihilation"
else:
data["Configurations"]["Default"]["MainFunction.Stage1"]=game
with open(setpath,"w",encoding="utf-8") as f:
json.dump(data,f)
os.system('start '+maapath)
time.sleep(60)
if ifoff():
return True
else:
command=killpath(maapath)
os.system(command)
os.system('start '+maapath)
time.sleep(60)
if ifoff():
return True
else:
command=killpath(maapath)
os.system(command)
return False
def updata(id):
db=sqlite3.connect(DATABASE)
cur=db.cursor()
cur.execute("SELECT * FROM adminx WHERE admin='%s'" %(id))
info=cur.fetchall()
cur.execute("UPDATE adminx SET day=%d WHERE admin='%s'" %(info[0][2]-1,id))
db.commit()
cur.execute("UPDATE adminx SET last='%s' WHERE admin='%s'" %(curdate,id))
print("upcurdate")
db.commit()
cur.close()
db.close()
return 0
#获取PATH与用户数据
DATABASE="data/data.db"
db=sqlite3.connect(DATABASE)
cur=db.cursor()
cur.execute("SELECT * FROM setting WHERE True")
path=cur.fetchall()
path=str(path[0][0])
setpath=path+"/config/gui.json"
logpath=path+"/debug/gui.log"
maapath=path+"/MAA.exe"
cur.execute("SELECT * FROM adminx WHERE True")
data=cur.fetchall()
data=[list(row) for row in data]
cur.close()
db.close()
#开始执行
curdate=datetime.date.today()
curdate=curdate.strftime('%Y-%m-%d')
idnew=[]
idold=[]
idfail=[]
for i in range(len(data)):
if data[i][3]=='y' and data[i][4]!=curdate and data[i][2]>0:
book=runmaa(data[i][1],data[i][5])
if book:
updata(data[i][0])
idnew.append(data[i][0])
print(colored("已完成"+data[i][0]+"今日的代理","yellow"))
else:
idfail.append(data[i][0])
print(colored("异常中止"+data[i][0]+"的代理","red"))
for i in range(len(data)):
if data[i][3]=='y' and data[i][4]==curdate and data[i][2]>0:
book=runmaa(data[i][1],data[i][5])
if book:
idold.append(data[i][0])
print(colored("已重复完成"+data[i][0]+"今日的代理","yellow"))
with open("log.txt","w", encoding="utf-8") as f:
print("任务结束,已完成数:"+str(len(idnew))+",未完成数:"+str(len(idfail))+",重复执行数:"+str(len(idold)),file=f)
if len(idfail)!=0:
print("代理未完成的用户:",file=f)
for i in range(len(idfail)):
print(idfail[i],file=f)

View File

@@ -1,7 +0,0 @@
声明:
KillSpecifiedProcess.cpp与mytaskkill.exe来自CSDN博主的文章遵循CC 4.0 BY-SA 版权协议附加以下信息:
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接https://blog.csdn.net/qq_29542611/article/details/122003681