Compare commits

..

1125 Commits

Author SHA1 Message Date
DLmaster361
33ee5c05a7 fix: 修复无法切换git分支问题 2025-10-02 22:43:07 +08:00
DLmaster361
7952e88885 fix: 强制进入后端时,仍尝试建立ws连接 2025-10-02 15:51:32 +08:00
DLmaster361
c1f7ea6922 fix(maa): 适配前端对话框ws反馈格式 2025-10-02 14:39:47 +08:00
MoeSnowyFox
196aa9873b fix(electron): 调整对话框窗口高度- 将对话框窗口的高度从140增加到145像素
(这个高度刚刚好解决html位置问题)
2025-10-02 14:06:43 +08:00
MoeSnowyFox
334c17c055 feat(dialog): 实现对话框主题与拖拽功能 2025-10-02 02:30:05 +08:00
MoeSnowyFox
0a20ee299d feat(dialog): 实现对话框主题与拖拽功能 2025-10-02 02:29:49 +08:00
7d1ceda958 Revert "feat(gitService): 根据app版本切换git拉取的分支"
This reverts commit 2f07c72025.
2025-10-02 01:31:17 +08:00
2f07c72025 feat(gitService): 根据app版本切换git拉取的分支 2025-10-02 01:08:44 +08:00
MoeSnowyFox
5da96242aa fix(user):修复用户编辑页面的WebSocket连接问题 2025-10-02 00:39:03 +08:00
MoeSnowyFox
5069a65559 Merge remote-tracking branch 'origin/v5.0.0-alpha.3' into feature/refactor
# Conflicts:
#	frontend/src/components/WebSocketMessageListener.vue
2025-10-01 23:42:38 +08:00
21d1874602 Merge branch 'feature/refactor' into v5.0.0-alpha.3 2025-10-01 23:41:04 +08:00
MoeSnowyFox
1d91204842 feat(electron): 添加系统级对话框功能
- 在 ElectronAPI 中新增对话框相关方法:showQuestionDialog、dialogResponse、resizeDialogWindow
- 实现独立的对话框窗口 dialog.html,支持自定义标题、消息和选项- 添加对话框窗口的键盘导航和焦点管理功能
- 在主进程中实现对话框窗口的创建、显示和响应处理
- 更新 WebSocket 消息监听器组件,使用系统级对话框替代应用内弹窗- 优化进程清理逻辑,增加多种清理方法并行执行
- 重构日志工具类,改进 Electron API 调用方式
- 调整 Git 更新检查逻辑,避免直接访问 GitHub
- 移除冗余的类型定义文件,统一 Electron API 接口定义
2025-10-01 23:24:39 +08:00
MoeSnowyFox
3d204980a2 docs(websocket): 添加useWebSocket参考文档 2025-10-01 23:24:38 +08:00
MoeSnowyFox
1eae8c80e5 feat(websocket): 实现WebSocket消息监听和调试功能
- 添加WebSocketMessageListener组件用于全局消息监听- 新增WebSocketDebugPanel调试面板(仅开发环境显示)
- 在多个组件中实现基于subscriptionId的WebSocket连接管理
-为MAA和通用配置添加更可靠的连接生命周期控制
- 引入消息测试页面用于调试WebSocket消息处理- 更新调度器逻辑以支持新的WebSocket订阅机制
- 优化WebSocket连接的创建、维护和断开流程
- 添加开发环境下的调度中心调试工具导入- 重构WebSocket相关组件的导入和注册方式
- 移除冗余的电源倒计时相关状态和方法
2025-10-01 23:24:38 +08:00
DLmaster361
a72ce489bd fix: 修正后端webhook测试逻辑 2025-10-01 20:56:32 +08:00
MoeSnowyFox
efa9f28aad docs(websocket): 添加useWebSocket参考文档 2025-10-01 18:06:24 +08:00
MoeSnowyFox
4beed5048a feat(websocket): 实现WebSocket消息监听和调试功能
- 添加WebSocketMessageListener组件用于全局消息监听- 新增WebSocketDebugPanel调试面板(仅开发环境显示)
- 在多个组件中实现基于subscriptionId的WebSocket连接管理
-为MAA和通用配置添加更可靠的连接生命周期控制
- 引入消息测试页面用于调试WebSocket消息处理- 更新调度器逻辑以支持新的WebSocket订阅机制
- 优化WebSocket连接的创建、维护和断开流程
- 添加开发环境下的调度中心调试工具导入- 重构WebSocket相关组件的导入和注册方式
- 移除冗余的电源倒计时相关状态和方法
2025-10-01 17:13:31 +08:00
DLmaster361
e286fc8d55 feat: 初步完成后端自定义webhook适配;重构配置项管理体系 2025-10-01 11:05:50 +08:00
Alirea
68b1ed4238 fix(api): 删除一个logger使用 2025-09-30 08:17:44 +08:00
Alirea
e5b43a9c45 fix(notif): 修复潜在的KeyError:index问题 2025-09-29 19:28:45 +08:00
Alirea
98a2b0f176 fix(notif): 修复一处TypeError参数错误 2025-09-29 13:26:34 +08:00
DLmaster361
06770eb3cc fix: 主要核心后端添加报错捕获机制 2025-09-28 20:38:54 +08:00
Alirea
dfc403733f feat(ui): 优先尝试使用用户选择的镜像源进行后端更新,最后使用github 2025-09-28 13:54:52 +08:00
Alirea
f423f3b577 fix(notif): 修复一处get错误和else位置 2025-09-28 13:54:52 +08:00
fcd8f042b7 refactor(package): update version to 5.0.0-alpha.2 2025-09-28 01:21:18 +08:00
Alirea
3140c29832 feat(ui): 新增初始化流程自动重试 2025-09-27 20:10:10 +08:00
Alirea
1c97228bc0 fix(notif): 修复通用调度脚本邮件信息丢失bug 2025-09-27 19:08:40 +08:00
DLmaster361
8ce747a839 Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-27 17:16:19 +08:00
DLmaster361
124d3e34d6 fix: 拆分用户通知的类型 2025-09-27 17:16:12 +08:00
837af62a60 refactor(webhook): 添加切换Webhook启用状态功能,优化UI交互 2025-09-27 17:14:47 +08:00
14a8a308c5 refactor(notify): 重构通知设置,支持自定义Webhook管理与模板配置 2025-09-27 16:54:50 +08:00
28ce06c92d refactor(notification): 优化模板解析逻辑,支持JSON和字符串模板处理 2025-09-27 16:48:46 +08:00
2d6dae3fc9 refactor(config): 重构通知配置项并优化Webhook获取逻辑 2025-09-27 16:32:54 +08:00
d2066e9631 feat(notify): 实现自定义Webhook通知功能 2025-09-27 16:22:04 +08:00
13caed7207 Revert "refactor: 添加自定义Webhook推送功能及模板管理"
This reverts commit 57a3505cab.
2025-09-27 15:36:20 +08:00
2c57426051 Revert "refactor: 添加自定义Webhook配置项"
This reverts commit bb602d92cf.
2025-09-27 15:36:16 +08:00
bb602d92cf refactor: 添加自定义Webhook配置项 2025-09-27 15:10:05 +08:00
57a3505cab refactor: 添加自定义Webhook推送功能及模板管理 2025-09-27 14:52:41 +08:00
MoeSnowyFox
2ab402e8ff feat:使用keyboard库 2025-09-27 04:47:54 +08:00
MoeSnowyFox
40d06cce54 Merge branch 'feature/refactor' of ssh://ssh.github.com:443/AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-27 03:34:17 +08:00
MoeSnowyFox
7b0307070e feat: 添加模拟器管理器 2025-09-27 03:32:43 +08:00
Alirea
09038aeb09 fix(notif): 修复理智回满时间不带多久回满的问题 2025-09-27 02:05:01 +08:00
Alirea
f869c19353 fix(notif): 真的修复了手贱删的括号 2025-09-27 01:35:02 +08:00
Alirea
b6fd72f0d8 fix(notif): 修复手贱删的括号 2025-09-27 01:34:15 +08:00
Alirea
f9192ef9eb fix(notif): 修复KeyError保错,兼容过往日志 2025-09-27 01:28:19 +08:00
Alirea
6f77c29e19 refactor(notif): 精简html模板文字标题 2025-09-26 23:39:37 +08:00
Alirea
4e44669695 feat(notif): 通知增加理智剩余量和理智恢复时间 2025-09-26 23:39:37 +08:00
DLmaster361
f04b7bf073 Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-26 14:30:09 +08:00
DLmaster361
0ac0225d7d fix: 优化设置页部分显示效果 2025-09-26 14:30:02 +08:00
Alirea
128cf3de4c feat(ui): 日志查看的导出日志改为log格式 2025-09-26 00:39:04 +08:00
DLmaster361
e528936c8d fix: 删除脚本或用户后同步删除对应保存的配置文件 2025-09-25 23:54:20 +08:00
DLmaster361
eed0319098 fix: 修复用户页面的MAA配置按钮与通用脚本配置按钮失效 2025-09-25 23:30:54 +08:00
74ea0af9bc refactor: 后端发送ws,收到后前端会自杀 2025-09-25 22:52:56 +08:00
69fac94058 Merge remote-tracking branch 'upstream/feature/refactor' into feature/refactor 2025-09-25 22:40:12 +08:00
29bd8a4aff refactor: 更新Git仓库检查逻辑,避免直接访问GitHub并使用镜像站进行pull操作 2025-09-25 22:40:00 +08:00
DLmaster361
54dc1d392d fix: 修复后端升级配置文件时部分字段未替换的问题 2025-09-25 21:44:32 +08:00
DLmaster361
5c55db7067 Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-25 19:54:18 +08:00
DLmaster361
bbe5286601 fix: 修复通用调度的配置路径跳变 2025-09-25 19:54:12 +08:00
Alirea
06b3147b53 feat(ui): 历史记录详细日志增加字体大小调整选项 2025-09-25 19:26:02 +08:00
Alirea
dbd1731d7f feat(ui): 历史记录详细日志修复过长被撑开到视界外 2025-09-25 18:12:36 +08:00
Alirea
320d7ab17d feat(ui): 历史记录设置最小高度 2025-09-25 17:24:16 +08:00
Alirea
4ad346ed14 fix(notif): 修改通知底部链接的文档站仓库为文档站 2025-09-25 17:16:05 +08:00
DLmaster361
f9f8b56d18 fix: 修复静默不生效问题 2025-09-25 08:15:27 +08:00
2101ae9908 refactor: 移动电源操作倒计时逻辑至全局组件 GlobalPowerCountdown.vue[待测试] 2025-09-25 00:43:03 +08:00
dbc5784691 refactor: 第一次启动直接进去手动配置模式 2025-09-25 00:06:51 +08:00
MoeSnowyFox
632ad33562 fix(Scripts.vue): 调整类型选项样式以隐藏不必要的分隔符
移除 AntD 在按钮包装器之间注入的小分隔符,并设置类型选项和模式选项的高度为自动以改善布局。
2025-09-24 23:39:03 +08:00
MoeSnowyFox
d3639d44e1 fix(plan): header-actions和header-left固定于同一行 2025-09-24 23:35:26 +08:00
af28869d2a refactor: 添加进程管理功能,支持强制清理相关进程和获取进程信息 2025-09-24 23:33:45 +08:00
DLmaster361
34df37c040 fix: 调度中心ws订阅的相关方法在初始化时暴露 2025-09-24 21:43:54 +08:00
bd58a512c9 refactor: 重构WebSocket消息处理逻辑,使用全局处理函数替代订阅方式 2025-09-24 19:15:19 +08:00
DLmaster361
cf4c8cfe17 fix: 修复定时任务的调度台创建方法 2025-09-24 18:11:48 +08:00
Alirea
ff8f501b98 feat(ui): 调试工具添加打开前端控制台快捷方式 2025-09-24 15:58:03 +08:00
Alirea
59de0ec510 fix(ui): 修复调度中心切换路由后信息丢失的问题 2025-09-24 15:52:52 +08:00
DLmaster361
cda50ea134 Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-24 01:01:58 +08:00
DLmaster361
97ec518e43 fix: ws消息允许接收多条遗留消息 2025-09-24 01:01:48 +08:00
d338210426 refactor: 添加电源操作显示更新逻辑,支持从后端接收状态并更新本地显示 2025-09-24 00:47:34 +08:00
b3fe49b8d1 Merge remote-tracking branch 'upstream/feature/refactor' into feature/refactor 2025-09-24 00:25:22 +08:00
96c022c2ec refactor: 重构电源操作倒计时逻辑,改为使用全屏弹窗并由后端控制 2025-09-24 00:25:13 +08:00
DLmaster361
0f1cbfc82d fix: 修复后端电源逻辑错误 2025-09-24 00:16:54 +08:00
MoeSnowyFox
53d91fb3f8 refactor(plan):优化计划类型标签显示逻辑并简化 API 调用
移除 `shouldShowPlanTypeTag` 方法中不必要的 `plan` 参数,因为其判断逻辑仅依赖于计划列表长度。同时简化 `usePlanApi.ts` 中的请求返回逻辑,避免多余的变量赋值。此外,统一了 API 模块的导入路径为 `@/api`,以提高路径一致性与可维护性。
2025-09-23 23:45:18 +08:00
MoeSnowyFox
7e74af93a0 refactor(plan): 统一使用 MaaPlanConfig 作为默认计划类型 2025-09-23 23:45:18 +08:00
2331d7e38b refactor: 注释掉杀死后端的函数,改为由后端自杀 2025-09-23 23:39:17 +08:00
6fd46087bb refactor: 添加关闭后端程序和取消电源任务的API接口 2025-09-23 23:27:49 +08:00
DLmaster361
76f330e4d3 feat: 后端添加电源操作逻辑 2025-09-23 16:00:13 +08:00
DLmaster361
1a541cbc63 Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-23 14:15:50 +08:00
DLmaster361
133ab0e78c feat: 后端添加关闭接口 2025-09-23 14:15:44 +08:00
MoeSnowyFox
35aa3cc42d feat(plan): 优化计划页面结构与加载逻辑
- 调整 template 结构,将加载状态与主要内容分离,提升可读性
- 引入 optionsLoaded 控制选项加载时机,避免初始渲染阻塞
- 增加 onMounted 和 watch逻辑确保异步数据正确处理- 统一计划类型为 MaaPlanConfig,兼容旧类型 MaaPlan- 优化 PlanHeader 新建计划按钮交互,支持动态显示按钮文本- 改进 PlanSelector 中计划类型标签显示逻辑,仅在必要时展示
2025-09-23 00:02:38 +08:00
MoeSnowyFox
4114e4ffc0 fix(plan):优化计划列表映射和保存逻辑
移除 planList 映射中未使用的 index 参数,简化代码逻辑。
调整 watch 回调执行时机,使用 flush: 'post' 确保在 DOM 更新后执行,并移除不必要的 nextTick 等待,直接调用防抖保存函数。
2025-09-22 22:41:39 +08:00
DLmaster361
22ebe7e51b fix: 后端添加定时任务防止重复启动机制 2025-09-22 15:52:11 +08:00
0437778c14 doc: 移动一下文档位置 2025-09-22 01:06:20 +08:00
70fc623f54 feat(scheduler): 实现TaskManager WebSocket消息自动创建调度台 2025-09-22 01:05:31 +08:00
MoeSnowyFox
32df65fb65 feat(plan):优化计划保存与切换逻辑,提升性能与用户体验- 在计划组件中引入防抖机制,避免频繁保存操作
- 实现异步保存队列,确保计划切换时数据不丢失
- 优化计划切换逻辑,支持后台保存并提升响应速度
- 在组件卸载前确保所有 pending 保存操作完成
- 修复 MaaPlanTable 中响应式丢失问题,优化选项缓存逻辑
- 为 PlanSelector 添加点击防抖,防止重复触发计划切换- 重构数据同步逻辑,提高表格与计划数据的同步效率
2025-09-21 23:56:27 +08:00
MoeSnowyFox
4f1e49ce85 fix(plan): 优化计划表格样式与交互 2025-09-21 23:37:27 +08:00
MoeSnowyFox
db0ac79e44 feat(plan):重构计划管理页面并新增 MAA 计划表组件
将原 Plans.vue 页面重构为 plan/index.vue
2025-09-21 23:12:18 +08:00
DLmaster361
d5331b728d fix: 后端ws不再raise无连接错误;修复定时逻辑 2025-09-21 18:09:01 +08:00
1c640c3df8 Merge remote-tracking branch 'upstream/feature/refactor' into feature/refactor 2025-09-21 15:18:22 +08:00
de45255feb fix: 修复任务总览的状态不能自动更新的问题 2025-09-21 15:17:40 +08:00
DLmaster361
1f89639463 Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-21 15:11:20 +08:00
DLmaster361
cb741c7b2e feat: 后端添加定时任务与启动时任务 2025-09-21 15:11:14 +08:00
6897f35d1e refactor: 重构调度中心的任务总览部分 2025-09-21 15:05:02 +08:00
7c34b3ca94 feat: 添加音频播放组件 2025-09-21 01:45:08 +08:00
DLmaster361
74a72961f7 fix: 重命名音效资源文件 2025-09-21 01:26:46 +08:00
DLmaster361
981c3a8624 feat: 后端挂载音频资源文件/api/res/sounds 2025-09-21 01:16:12 +08:00
DLmaster361
a16f9b2da4 Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-21 01:08:41 +08:00
DLmaster361
1f85360c92 fix: 调度中心界面小范围优化 2025-09-21 01:08:33 +08:00
MoeSnowyFox
5f7cdd6a57 fix(ScriptTable):在脚本顺序未更改的情况下不触发脚本排序保存 2025-09-21 00:46:18 +08:00
Alirea
887f10ef3f fix(ui): 又给你加回来加载界面,但让他不会nodata了 2025-09-21 00:29:37 +08:00
MoeSnowyFox
14c61815c6 fix(Scripts.vue): 优化WebSocket消息处理逻辑
修复语法错误(但愿逻辑不要炸)
2025-09-21 00:13:26 +08:00
MoeSnowyFox
4abce146a9 fix(Scripts.vue): 移除冗余的加载状态组件 2025-09-21 00:09:38 +08:00
MoeSnowyFox
0e8094e52f fix(dev): 更新调试面板组件导入路径 2025-09-21 00:06:35 +08:00
MoeSnowyFox
f3680a8274 refactor(devtools): 重构调试面板为模块化组件 2025-09-20 23:58:03 +08:00
MoeSnowyFox
cd706640d9 feat(Scripts.vue): 添加MAA配置遮罩层并优化样式布局 2025-09-20 23:43:17 +08:00
MoeSnowyFox
e0ba88d7b0 feat(layout): 实现路由锁定功能并优化侧边栏样式
- 新增 `useRouteLock` 组合式函数,用于在表单编辑等场景下锁定路由切换- 在 `AppLayout.vue` 中集成路由锁定逻辑,防止用户误操作离开当前页面
- 优化侧边栏菜单样式,增强可读性与维护性
- 调整代码格式,提升模板和样式部分的可读性
2025-09-20 23:43:17 +08:00
MoeSnowyFox
4cc1d1186e feat(DevDebugPanel): 优化调试面板的拖拽和展开收起交互 2025-09-20 23:43:17 +08:00
DLmaster361
5eab1b0986 fix: 重新修正ws消息分发逻辑 2025-09-20 23:32:06 +08:00
DLmaster361
60e8ac0ce9 fix: 添加ws消息队列避免消息漏收 2025-09-20 18:41:25 +08:00
DLmaster361
a5e09bc489 fix: 重整调度中心的UI 2025-09-20 17:02:51 +08:00
MoeSnowyFox
199907eb26 feat(useSchedulerLogic):修复新建队列循环调用导致卡死问题 2025-09-19 23:58:59 +08:00
MoeSnowyFox
969e223fb4 feat(components): 添加开发环境调试面板组件 2025-09-19 23:53:15 +08:00
DLmaster361
e62b9b3943 fix: 初步完成调度中心样式优化 2025-09-19 21:36:09 +08:00
DLmaster361
a9769c6397 fix: 调整调度队列的空状态 2025-09-19 12:23:53 +08:00
DLmaster361
d21d2f9a1d Revert "feat(QueueItemManager, TimeSetManager): 增加定时列表和任务列表的拖拽功能"
This reverts commit e569930287.
2025-09-19 10:34:36 +08:00
DLmaster361
bd6d7e8189 Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-19 10:21:54 +08:00
DLmaster361
c0af9d7de0 fix: 移除任务列表骨架屏 2025-09-19 10:20:54 +08:00
e569930287 feat(QueueItemManager, TimeSetManager): 增加定时列表和任务列表的拖拽功能 2025-09-19 01:45:40 +08:00
5cd0ca29a4 refactor(index.vue): 格式化一下css 2025-09-19 01:29:49 +08:00
b3bb43dcdf refactor(TabFunction): 整理设置项,整合一下 2025-09-19 01:26:43 +08:00
01ee70f3fb refactor(ScriptTable): 添加脚本和脚本用户的拖拽功能 2025-09-19 01:06:17 +08:00
DLmaster361
ed6b52451b fix: 后端移除队列项冗余校验 2025-09-19 00:34:06 +08:00
DLmaster361
6150142d62 fix: 后端修正脚本下拉框未选择值 2025-09-19 00:15:43 +08:00
DLmaster361
bf99e41cee Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-19 00:13:02 +08:00
DLmaster361
4db49e0bb9 feat: 初步优化调度队列样式 2025-09-19 00:12:55 +08:00
MoeSnowyFox
f559a71de2 refactor(Scripts): 优化脚本页面代码格式和逻辑
- 删除多余的空行
- 添加必要的逗号
- 修复用户状态更新逻辑
2025-09-18 23:57:44 +08:00
DLmaster361
b5b208c611 fix: 配置分享站换链 2025-09-18 12:15:41 +08:00
7da7a8b4f7 refactor: 添加加载状态提示,删掉白色背景 2025-09-18 00:39:28 +08:00
MoeSnowyFox
010d99ce78 feat(router): 添加路由跳转工具函数并优化计划页面跳转逻辑 2025-09-18 00:08:11 +08:00
Alirea
9946a8376a fix(ui): 删除加载界面,让他不会瞎闪了 2025-09-17 23:33:10 +08:00
Alirea
b11550c500 fix(ui): 修复历史记录nodata图片不加载的问题 2025-09-17 01:48:30 +08:00
Alirea
1e8ada6c36 feat(typescript): 新增一个asset类型声明 2025-09-17 01:47:22 +08:00
Alirea
aacf32ac61 feat(ui): 重布局历史记录,使日志拥有更大的横向空间 2025-09-17 01:16:23 +08:00
Alirea
308bb83bec feat: 简化活动活动剩余时间显示 2025-09-17 00:31:16 +08:00
DLmaster361
4dff160b82 fix(task): 修复后端在零用户情况下index缺失引发的故障 2025-09-17 00:13:05 +08:00
DLmaster361
cd1d114cac Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-17 00:00:22 +08:00
DLmaster361
f36d647594 fix(ui): 修复计划表深色模式异常 2025-09-17 00:00:16 +08:00
MoeSnowyFox
edadc6f7ce feat(router): 更新路由配置并添加调度中心页面
- 更新路由配置,将 Scheduler 组件路径修改为 ../views/scheduler/index.vue
2025-09-16 23:59:08 +08:00
DLmaster361
1585ab564d feat: 设置脚本时不再检测脚本运行状态,由用户手动保存 2025-09-16 21:04:36 +08:00
DLmaster361
6c52d5a2f0 Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-16 20:36:23 +08:00
DLmaster361
63fd3d590b feat: 后端添加严格的下拉表数据校验逻辑,添加新ws消息类型task_dict 2025-09-16 20:36:16 +08:00
Alirea
139fd0bcc3 feat: 让活动剩余时间过少时的倒计时更加炫酷 2025-09-16 18:37:03 +08:00
Alirea
481a204a19 feat: 默认使用自动代理 2025-09-16 17:53:48 +08:00
DLmaster361
62877257e4 feat: 后端优化部分表的相互交互逻辑,自动移除失效的表单 2025-09-16 15:44:17 +08:00
2a493bd62a refactor: 优化标题栏样式,增强更新提示的视觉效果和交互体验 2025-09-16 00:23:21 +08:00
249e8c1a08 refactor: 清理冗余样式,优化组件结构和响应式设计 2025-09-16 00:11:09 +08:00
2d44389587 feat: 添加上传脚本配置功能,支持用户分享配置到云端 2025-09-15 23:54:58 +08:00
84ec172871 refactor: 拆分脚本编辑页面 2025-09-15 23:23:57 +08:00
ac9418f787 refactor: 重构主窗口创建逻辑,优化显示器适配和最小尺寸计算 2025-09-15 22:51:16 +08:00
Alirea
e79830565e style: 修改默认窗口大小与最小窗口大小 2025-09-15 14:02:46 +08:00
Alirea
4298961311 style: 增加资源关卡最大显示数到5,在1080P下达到最佳效果,并修改响应式布局 2025-09-15 14:02:46 +08:00
Alirea
cec8944192 feat: 优化初始化逻辑,自动模式下代码没更新不执行依赖安装 2025-09-15 14:02:46 +08:00
DLmaster361
6b78162784 fix: 初步适配日志栏 2025-09-15 10:50:25 +08:00
Alirea
7ca0dcc918 feat: 优化初始化逻辑,仅在自动模式下出现环境缺失页面 2025-09-15 10:17:27 +08:00
DLmaster361
804c3ed62c fix: 移除调试代码 2025-09-15 00:15:55 +08:00
DLmaster361
aaee9aa1ba fix: 尝试修复ws消息不显示的问题
- 但是失败了
2025-09-15 00:10:08 +08:00
660b82da7a feat: 拆分MAAUserEdit 2025-09-15 00:09:01 +08:00
MoeSnowyFox
d0d5d76f10 Merge remote-tracking branch 'origin/feature/refactor' into feature/refactor
# Conflicts:
#	frontend/src/views/MAAUserEdit.vue
2025-09-14 23:34:12 +08:00
MoeSnowyFox
4026b35636 🚸 脚本界面计划表悬浮显示信息 2025-09-14 23:31:37 +08:00
2558cfca75 Merge remote-tracking branch 'upstream/feature/refactor' into feature/refactor 2025-09-14 23:24:53 +08:00
4b4aa8ba84 fix: 修复Git更新检查逻辑,添加仓库状态判断和更新提示 2025-09-14 23:24:05 +08:00
DLmaster361
1ae2a9bb87 fix: 再次修复上一条 2025-09-14 22:39:32 +08:00
DLmaster361
3b9d27383b fix: 修复MAA运行时错误获取user_id 2025-09-14 22:34:06 +08:00
MoeSnowyFox
17d7e3bad0 🚸 统一管理时间, 修复计划表按时间显示的错误 2025-09-14 21:57:03 +08:00
DLmaster361
8ea6c9565e Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-14 21:44:13 +08:00
DLmaster361
67b327e192 feat: 初步完成调度台适配 2025-09-14 21:42:56 +08:00
MoeSnowyFox
f98a749efa 🚸 计划表弹窗'已切换到计划模式'显示name而非uid 2025-09-14 20:01:37 +08:00
MoeSnowyFox
9d067f1c6a 🐛 通知测试使用标准api 2025-09-14 17:54:16 +08:00
DLmaster361
d1fdd5f672 feat: 后端添加自动代理队列的电源ws消息 2025-09-14 16:05:31 +08:00
DLmaster361
76fcc13d4a feat: 把历史记录调到下方 2025-09-14 15:54:12 +08:00
DLmaster361
76b32ad2c1 Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-14 15:47:00 +08:00
DLmaster361
ad56904e56 feat: 初步完成计划表样式更新 2025-09-14 15:46:54 +08:00
5f12ddf332 refactor: 日志和镜像测试页面加入返回按钮;镜像站配置移动到高级设置中 2025-09-14 14:13:40 +08:00
4d8d71f294 refactor: 重构环境检查逻辑,添加环境不完整提示页面 2025-09-14 02:04:31 +08:00
a15b2bd8ce refactor: 优化镜像源加载逻辑,使用云端数据替代静态配置 2025-09-14 01:24:31 +08:00
820649225d refactor: 修复深色模式下背景看不清的问题 2025-09-14 01:16:51 +08:00
DLmaster361
d0651e2104 feat: 再次移除UpdateType字段 2025-09-14 00:51:54 +08:00
MoeSnowyFox
8910e09493 Merge remote-tracking branch 'origin/feature/refactor' into feature/refactor
# Conflicts:
#	frontend/src/views/Settings.vue
2025-09-14 00:42:54 +08:00
MoeSnowyFox
eafd6eb808 🎨 拆分settings.vue至/setting/...中 2025-09-14 00:41:54 +08:00
DLmaster361
e07c1a844a Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-14 00:37:49 +08:00
DLmaster361
6846415899 feat: 移除更新类型字段 2025-09-14 00:37:43 +08:00
MoeSnowyFox
4e2419a5f1 Merge remote-tracking branch 'origin/feature/refactor' into feature/refactor 2025-09-14 00:01:32 +08:00
MoeSnowyFox
1866495379 发送通知测试 2025-09-13 23:57:56 +08:00
DLmaster361
c4d1ed3184 Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-13 23:32:33 +08:00
DLmaster361
aa6836d26d feat: 修整计划表样式 2025-09-13 23:32:26 +08:00
e49d67f501 refactor: 修改按钮文案并移除未使用的更新状态按钮 2025-09-13 17:48:13 +08:00
DLmaster361
1ab865df7b Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-13 17:43:41 +08:00
DLmaster361
b36fb89566 feat: 自动更新按钮生效 2025-09-13 17:43:36 +08:00
94dea445cf feat: 支持自定义镜像源添加、删除及重试机制 2025-09-13 17:26:39 +08:00
c8380ddb90 feat: 添加镜像配置管理功能及测试页面 2025-09-13 17:06:57 +08:00
DLmaster361
6c7a0226fd feat: 前端更新改为定时拉取 2025-09-13 14:54:11 +08:00
DLmaster361
79bd982383 feat: 修改检查更新按钮位置 2025-09-13 11:15:22 +08:00
DLmaster361
e097e40826 fix: 版本信息转临时存储;允许强制请求版本信息 2025-09-13 11:01:26 +08:00
DLmaster361
642b34eca3 feat: 添加版本信息缓存机制 2025-09-13 10:35:36 +08:00
fbd7fa6a00 refactor: 更新检查更新按钮UI,调整部分文案 2025-09-13 00:10:00 +08:00
MoeSnowyFox
19537698eb 🗑️ 迁移github地址 2025-09-12 23:34:57 +08:00
805783c85b fix: 测试时十分钟改为一分钟忘记改回去了~ 2025-09-12 20:52:43 +08:00
fd12a6359d Merge remote-tracking branch 'upstream/feature/refactor' into feature/refactor 2025-09-12 00:27:36 +08:00
04158a40bc feat: 添加测试通知API接口 2025-09-12 00:27:15 +08:00
DLmaster361
a10c927253 Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-12 00:10:43 +08:00
DLmaster361
b16bf0fe3f fix: 使用虚拟环境的git 2025-09-12 00:10:38 +08:00
MoeSnowyFox
47e7291885 🗑️ 修复警告 2025-09-11 23:35:42 +08:00
MoeSnowyFox
a034f10a19 🗑️ 移除ws旧版本相关代码 2025-09-11 23:25:08 +08:00
DLmaster361
80eb0bc49d fix: 更新检查后端限制4h请求一次 2025-09-11 23:06:23 +08:00
DLmaster361
90b8020c0a fix: 修正ws超时参数 2025-09-11 22:10:20 +08:00
DLmaster361
a33e2675dd fix: 修复ws相关逻辑 2025-09-11 21:57:24 +08:00
DLmaster361
ed81cc65ce Merge branch 'feature/refactor' of github.com:AUTO-MAS-Project/AUTO-MAS into feature/refactor 2025-09-10 23:07:59 +08:00
DLmaster361
fd8f602da2 fix: 完成AUTO-MAS改名 2025-09-10 23:07:52 +08:00
4b4be6fa0f feat: 添加版本检查功能及更新提示模态框 2025-09-10 22:20:39 +08:00
Alirea
ab48b91f26 test:push test 2025-09-10 21:03:10 +08:00
Alirea
8ae2e82d18 fix: 修改历史记录标题颜色大小等,统一效果 2025-09-10 20:51:17 +08:00
Alirea
d43d96e8eb fix: 删除部分组件的最大宽度限制,统一效果 2025-09-10 20:51:17 +08:00
DLmaster361
d207c65df7 feat: 添加通知测试接口 2025-09-10 20:33:05 +08:00
6138fc47b1 feat: 新增更新弹窗,以实现更新的下载 2025-09-10 20:18:11 +08:00
d9aa6305da docs: update README 2025-09-10 19:52:52 +08:00
DLmaster361
80aa2873f7 test: push test 2025-09-10 19:49:16 +08:00
MoeSnowyFox
3b7b3b8f74 Merge remote-tracking branch 'origin/feature/refactor' into feature/refactor 2025-09-10 00:11:39 +08:00
MoeSnowyFox
b4e4c97cbf 🚧 上传时同时打包exe和zip 2025-09-10 00:10:34 +08:00
DLmaster361
e3c0add012 fix: 矫正后端需要更新的逻辑 2025-09-09 23:14:29 +08:00
7237523237 fix: 逻辑写反了,修复一下 2025-09-09 21:40:46 +08:00
81d9d3d66c Merge remote-tracking branch 'upstream/feature/refactor' into feature/refactor 2025-09-09 21:28:23 +08:00
86f8884e45 feat: 添加后端版本检查功能及更新提示 2025-09-09 21:28:14 +08:00
MoeSnowyFox
88bc883fe1 Merge remote-tracking branch 'origin/feature/refactor' into feature/refactor 2025-09-09 21:21:22 +08:00
482077c862 feat: 添加获取后端git版本信息接口及相关类型定义 2025-09-09 21:16:26 +08:00
MoeSnowyFox
5c4fd4024b 💄 router-view增大间距至32px 2025-09-09 21:11:39 +08:00
75ff858f68 Merge remote-tracking branch 'upstream/feature/refactor' into feature/refactor 2025-09-09 21:10:49 +08:00
64fb077d65 feat: 添加炫彩更新提示~ 2025-09-09 21:10:41 +08:00
DLmaster361
d2e45038fd Merge branch 'feature/refactor' of github.com:DLmaster361/AUTO_MAA into feature/refactor 2025-09-09 21:00:17 +08:00
DLmaster361
e3c1143c0c feat: 添加后端版本号接口 2025-09-09 21:00:11 +08:00
fb4dc7fa83 feat: 导入更新检查和下载接口 2025-09-09 20:45:35 +08:00
MoeSnowyFox
3490eff76e Merge remote-tracking branch 'origin/feature/refactor' into feature/refactor 2025-09-09 20:20:27 +08:00
MoeSnowyFox
54cdef691a 💄 更改侧边栏ui 2025-09-09 20:20:03 +08:00
DLmaster361
4f8c12292b feat: 添加更新接口 2025-09-09 20:00:24 +08:00
AoXuan
1fd357a49c Merge pull request #74 from Zrief/z
配色修改
2025-09-09 08:22:30 +08:00
MoeSnowyFox
e144d62cdb 💄 更改侧边栏ui, 拉大字体, 增加响应式设计 2025-09-09 00:11:29 +08:00
Zrief
a4472b4f9c Merge branch 'feature/refactor' into z 2025-09-09 00:06:31 +08:00
Zrief
8de3405aa5 Update tag color and remove background style
修改碳关卡的颜色,适配浅色主题
删除指定的css配色,适配深色主题
2025-09-08 23:59:47 +08:00
Zrief
afd450eee6 Fix tag color and resolve merge conflict in Scheduler.vue
Changed the tag color for '碳-5' from 'none' to 'default' in Plans.vue. Also resolved a merge conflict in Scheduler.vue related to the .plan-header class styling.
2025-09-08 23:39:53 +08:00
DLmaster361
6798d896bc Merge branch 'feature/refactor' of github.com:DLmaster361/AUTO_MAA into feature/refactor 2025-09-08 23:06:53 +08:00
DLmaster361
b38c81b08c fix: 修复通用用户自定义脚本路径配置项初值 2025-09-08 23:06:47 +08:00
364e7a273f feat: 移除electron-updater依赖 2025-09-08 22:55:13 +08:00
a0c5334f57 feat: 添加发布脚本,添加electron-updater依赖 2025-09-08 22:42:56 +08:00
DLmaster361
41bb159542 fix: 后端对用户名进行文件夹合法性校验 2025-09-08 19:30:28 +08:00
DLmaster361
706bb8584d fix: 添加默认值校验机制 2025-09-08 15:42:45 +08:00
DLmaster361
a43b12bc17 fix: 任务的脚本列表补充完成状态 2025-09-08 14:57:40 +08:00
MoeSnowyFox
cbba670eb2 💄 更改顶边ui和侧边栏ui 2025-09-08 00:03:52 +08:00
MoeSnowyFox
ac182a7c77 🐛 修复调度中心ws逻辑 2025-09-07 23:17:52 +08:00
MoeSnowyFox
9c4e8d256a 🐛 ws统一链接 2025-09-07 19:20:06 +08:00
MoeSnowyFox
4a8fa68632 🐛 ws统一链接 2025-09-07 19:19:12 +08:00
DLmaster361
6bf30b4dc6 feat: 后端添加version上报 2025-09-07 03:21:38 +08:00
2636858fc0 Merge remote-tracking branch 'upstream/feature/refactor' into feature/refactor 2025-09-06 02:14:59 +08:00
250cccc0b9 feat: 添加自定义关卡功能,支持动态输入和选择 2025-09-06 02:14:50 +08:00
DLmaster361
f15ed86852 feat: 同步用户与脚本配置的样式 2025-09-06 02:14:45 +08:00
AoXuan
33a2cee5ef Merge pull request #73 from Zrief/z
为 “计划” 页面初步添加简化视图模式
2025-09-06 01:38:25 +08:00
DLmaster361
bd94a26a15 fix: 移除历史记录多余的图标 2025-09-06 01:36:56 +08:00
Zrief
02788d3bfc 为 “计划” 页面初步添加简化视图模式
为 Plans.vue 页面引入 “简化视图”(类mower)模式,支持用户在计划管理时,在配置视图与简化视图之间进行切换。
添加相关 UI 控件,实现阶段(stages)批量启用 / 禁用的逻辑,以及配套样式。
更新 .gitignore 文件,将 .yarn/install-state.gz 文件纳入忽略列表(即不纳入 Git 版本控制)。
2025-09-06 01:32:50 +08:00
fc8afe6624 refactor: 彻底删除UserEdit 2025-09-06 01:17:39 +08:00
DLmaster361
93a61232cf feat: 优化历史记录样式 2025-09-06 01:06:12 +08:00
DLmaster361
c4cd53277f fix: 通用脚本用户添加额外展示项 2025-09-05 17:41:50 +08:00
DLmaster361
f3e5a03a0f feat: 初始化界面添加标题栏 2025-09-05 16:03:27 +08:00
DLmaster361
bf62c25c93 fix: 完善全局设置UI 2025-09-05 14:01:37 +08:00
b9a7b6a889 feat: 完成记忆窗口位置,最小化到托盘和托盘区图标 2025-09-05 00:27:37 +08:00
DLmaster361
1ffd8c1a5a fix: 修复活动关卡时间判断 2025-09-04 19:28:47 +08:00
DLmaster361
58bd4a1765 fix: 优化调度队列显示效果 2025-09-04 17:01:58 +08:00
DLmaster361
278800c077 fix: 修整通用用户额外脚本样式 2025-09-04 15:53:06 +08:00
DLmaster361
8142c0398f feat: 通用脚本添加路径设置同步机制 2025-09-04 14:16:21 +08:00
DLmaster
5e2224dfdb Merge pull request #70 from Alirea10/feature/refactor
fix: 修复活动结束后的显示
2025-09-04 10:26:53 +08:00
Alirea
a206bb9e5c Merge remote-tracking branch 'fork/feature/refactor' into feature/refactor 2025-09-04 10:23:49 +08:00
Alirea
79feb73b27 fix: 修复活动结束后的显示 2025-09-04 10:17:59 +08:00
DLmaster361
97678368e1 fix: 修正ws消息类型 2025-09-04 00:34:24 +08:00
DLmaster361
678e09e536 feat: 启用新icon 2025-09-04 00:13:35 +08:00
DLmaster361
377e5250f9 fix: 调整脚本根目录 2025-09-03 22:54:38 +08:00
003f150a74 refactor: 增加TitleBar组件,微调页面布局 2025-09-03 22:44:16 +08:00
DLmaster361
54e289ce56 fix: 修复通用脚本配置文件路径选择无法切换 2025-09-03 22:06:50 +08:00
f26d336195 feat: UserEdit拆分成General和MAA两个文件,方便后期维护 2025-09-03 19:12:25 +08:00
DLmaster361
4fa7313b4f fix: 修整关卡号选择展示效果 2025-09-03 17:33:48 +08:00
DLmaster361
a0a2998fcb fix: 调整主页的空状态 2025-09-03 17:13:35 +08:00
DLmaster361
5007939d1c fix: 调整关卡号开放信息缩进 2025-09-03 16:57:41 +08:00
DLmaster361
c10d39ba83 fix: 修整通用脚本文案 2025-09-03 15:42:11 +08:00
DLmaster361
d0e0af9ed7 fix: ws连接强制为单例 2025-09-03 11:58:44 +08:00
DLmaster361
1a4a6fd5a3 fix: ws连接添加应用层ping-pong机制 2025-09-03 11:53:37 +08:00
DLmaster361
f1c3818249 feat: 修整无数据状态的显示效果 2025-09-02 23:13:01 +08:00
DLmaster361
eabd8b21a1 Merge branch 'feature/refactor' of github.com:DLmaster361/AUTO_MAA into feature/refactor 2025-09-02 21:52:50 +08:00
DLmaster361
09a294b38f feat: 修整用户空白页 2025-09-02 21:52:43 +08:00
949044fcdb fix: 修复通用脚本创建时,内容没有被正确获取;修复通用脚本用户添加时报错 2025-09-02 20:46:44 +08:00
272fd9b425 refactor: update RemainedDay default value to -1 2025-09-02 20:23:27 +08:00
6787b015c5 refactor: 调整setting样式。更新gitignore 2025-09-02 20:21:39 +08:00
DLmaster361
8a21b3da9b fix: 账号ID为非必填项 2025-09-02 20:12:04 +08:00
1473fa64ed refactor: update package manager to yarn@4.9.1 in package.json 2025-09-02 20:06:27 +08:00
8eb27555f5 refactor: 脚本下的用户全部展示(之前是三个) 2025-09-02 19:54:25 +08:00
05c90689e5 refactor: 修复一下创建用户时,id传入失败 2025-09-02 19:52:01 +08:00
07adbfd68c Revert "refactor: 表单的用户名不是必须"
This reverts commit 375cf3286a.
2025-09-02 19:39:51 +08:00
375cf3286a refactor: 表单的用户名不是必须 2025-09-02 19:33:13 +08:00
2fec029cb7 Merge remote-tracking branch 'upstream/feature/refactor' into feature/refactor 2025-09-02 19:28:31 +08:00
0f4ea4786f refactor: 微调一下日志查看功能的样式 2025-09-02 19:28:09 +08:00
DLmaster361
d74e42c281 Merge branch 'feature/refactor' of github.com:DLmaster361/AUTO_MAA into feature/refactor 2025-09-02 19:26:47 +08:00
DLmaster361
b926880ae9 Merge branch 'feature/refactor' of github.com:DLmaster361/AUTO_MAA into feature/refactor 2025-09-02 19:26:41 +08:00
DLmaster361
ffd13e8362 fix: 修整MAA脚本配置文案 2025-09-02 19:26:05 +08:00
baf8a642b8 refactor: 日志查看功能&优化日志功能 2025-09-02 19:26:04 +08:00
6b00c8a6be refactor: 添加回log功能~这次好用了~ 2025-09-02 18:20:20 +08:00
f4fe4ec019 refactor: 添加后端停止功能,优化后端启动逻辑 2025-09-02 17:17:37 +08:00
17cc1fa8a3 refactor: 移除logger。全部改为console输出 2025-09-02 16:58:53 +08:00
c91cd55586 refactor(.gitignore): add dist-electron to ignore list 2025-09-02 16:23:34 +08:00
24b9dc98d0 refactor(package): 打个补丁~修复一下不能自动编译的问题 2025-09-02 16:19:49 +08:00
909af77649 refactor(services): 补一下js 2025-09-02 16:09:43 +08:00
c8b1bf6d08 refactor(mirrors): 修改git推荐镜像站,移除无效镜像站 2025-09-02 15:09:11 +08:00
7f622de857 refactor(components): 修复代码错误,重新格式化代码 2025-09-02 14:50:24 +08:00
b586e311be refactor(steps): 调整一下样式,默认窗口高度调整为1000 2025-09-02 13:33:27 +08:00
11e3c2281e refactor(steps): 优化初始化页面的布局。让IDE能够正确识别@ 2025-09-02 13:26:06 +08:00
a40fa37bfd refactor(Queue, Scripts): 修改脚本页面的空状态 2025-09-02 12:45:44 +08:00
0162e1a9b7 refactor(ScriptTable): 备注最大长度调整为10汉字/字母 2025-09-02 01:41:53 +08:00
68df1d5332 refactor(package.json): remove unnecessary packageManager entry 2025-09-02 01:27:26 +08:00
DLmaster361
bd8a26a5ae Merge branch 'feature/refactor' of github.com:DLmaster361/AUTO_MAA into feature/refactor 2025-09-02 01:22:07 +08:00
DLmaster361
4125c93267 fix: 调整常驻开放关位置 2025-09-02 01:22:02 +08:00
b4ebf2ccc1 refactor(History, Settings): 去掉搜索前面的冒号;去掉不应该存在的设置项 2025-09-02 01:17:22 +08:00
6941850d90 refactor(ScriptEdit): 修改日志路径选择功能,移除无用样式 2025-09-02 01:01:05 +08:00
329ebd8633 refactor(Scripts, ScriptTable, UserEdit): 清理一下无用css,调整部分按钮位置 2025-09-02 00:54:30 +08:00
MoeSnowyFox
2ac631a18d Merge remote-tracking branch 'origin/feature/refactor' into feature/refactor 2025-09-02 00:28:57 +08:00
MoeSnowyFox
0e47461c3c 🍻 ws链接修改为统一端口(ai生成, 未检查) 2025-09-02 00:28:36 +08:00
1d02894957 Revert "fix: 复原疑似被误处理的文件"
This reverts commit 42710ecf60.
2025-09-02 00:02:43 +08:00
76aff5d627 refactor(Home): 主页图片走后端获取,修改git clone的地址 2025-09-02 00:00:51 +08:00
DLmaster361
42710ecf60 fix: 复原疑似被误处理的文件 2025-09-01 23:50:35 +08:00
DLmaster361
e98ebe3e84 Merge branch 'feature/refactor-backend' into feature/refactor 2025-09-01 23:46:52 +08:00
DLmaster361
716d864343 fix: 关卡号报错 2025-09-01 23:39:08 +08:00
432ec7b9b1 refactor(Scripts, UserEdit): 处理一下IDE的类型报错,然后给脚本管理页面加了个loading~ 2025-09-01 23:27:18 +08:00
7b74098a09 refactor(UserEdit): 修改基建导入部分 2025-09-01 22:58:02 +08:00
DLmaster361
39c8db0846 fix: 添加基建配置文件路径实际逻辑 2025-09-01 22:48:48 +08:00
DLmaster361
919f273970 fix: 初步将基建文件改为只保存路径 2025-09-01 22:36:42 +08:00
DLmaster361
dd0fc19f5d feat: 添加常规关卡开放日信息 2025-09-01 22:31:36 +08:00
ed3edd5eff refactor(UserEdit): 统一关卡选项加载逻辑 2025-09-01 22:28:42 +08:00
DLmaster361
f45aff61fd fix: 图片可直接通过url访问 2025-09-01 22:17:20 +08:00
e4cbbbcdea refactor(UserEdit): 优化关卡配置模式选择,动态加载选项 2025-09-01 22:10:10 +08:00
DLmaster361
88006ec5f4 feat: 添加掉落物的url查询API 2025-09-01 21:26:40 +08:00
49821abc12 refactor(Home): 修改PR关卡展示图标 2025-09-01 19:16:59 +08:00
DLmaster361
ee8075e4b8 fix: 修整掉落物信息 2025-09-01 16:45:06 +08:00
f41f066abc feat(Scripts): 添加通用脚本创建方式选择和模板导入功能 2025-09-01 01:06:18 +08:00
7575909bad fix(main): 修复一个类型错误 2025-09-01 00:04:15 +08:00
8d044e684a refactor(UserEdit): 优化文件选择逻辑,为空不显示选择成功 2025-08-31 23:55:52 +08:00
88374e5da4 feat(UserEdit): 添加基建配置文件选择和导入功能 2025-08-31 23:47:30 +08:00
c057c2ae21 fix(Plans): 修复连战次数为0时应该是不选择 2025-08-31 23:19:34 +08:00
c5fd0c1253 feat: 添加公告功能,支持查看公告和在系统浏览器中打开链接 2025-08-31 23:11:24 +08:00
fe35e37371 feat: 添加公告系统,支持动态显示和确认功能 2025-08-31 22:50:28 +08:00
fab4645132 refactor(Home.vue): 增强显示效果,活动剩余时间小于两天时红色毫秒显示 2025-08-31 22:23:47 +08:00
5d87a86a33 refactor(Home.vue): 适配新图片形式,调整倒计时组件 2025-08-31 22:16:49 +08:00
f281ceb5ef refactor: 更新后端接口api 2025-08-31 21:59:56 +08:00
2a77e33206 feat: 添加掉落物图片 2025-08-31 21:56:45 +08:00
DLmaster361
fe3fe6398d feat: 添加掉落物资源 2025-08-31 01:46:00 +08:00
f6c1e591f0 refactor(queue): 增加完成后操作配置功能,优化队列管理交互 2025-08-31 00:49:36 +08:00
f28b4cbad0 refactor(queue): 优化队列管理页面的布局和交互,增加开关配置功能 2025-08-30 20:59:51 +08:00
DLmaster361
f62740f20b fix: 修整所有日志与报错为中文 2025-08-30 20:30:43 +08:00
c15c74895b refactor(plans): 重构一下计划管理页面 2025-08-30 17:03:22 +08:00
7db6fcbcc8 refactor(config): 调整配置项的数值范围并增加服务器选项 2025-08-30 15:42:03 +08:00
7fe2ae2acc refactor(views): 优化空状态展示效果
- 在 Plans、Queue 和 Scripts 页面中添加统一的占位符样式
- 用 "placeholder" 替代 "empty state" 的概念,提升用户体验
- 增加新建按钮,方便用户快速创建计划、队列或脚本
- 优化文字描述,更加友好地引导用户进行操作
2025-08-30 15:04:32 +08:00
ab7afb3d3b feat(首页): 新增资源收集关卡功能
- 在首页添加资源收集关卡卡片,展示资源收集相关数据
- 优化活动关卡卡片,增加结束时间和剩余时间显示
- 调整页面布局,使活动关卡和资源收集关卡卡片更加清晰
- 优化图片加载逻辑,增加错误处理
2025-08-30 14:25:44 +08:00
DLmaster361
389e06cf9e fix: 调整芯片掉落物信息 2025-08-29 20:12:57 +08:00
DLmaster361
05abc0e678 fix: 常规关卡掉落物名修正 2025-08-29 14:38:10 +08:00
DLmaster361
1fa9cbeb7a fix: 关卡号变量由ss统一改为ActivityStage 2025-08-29 12:33:58 +08:00
DLmaster361
928746fe88 feat: 下载站改用域名访问 2025-08-29 12:30:30 +08:00
96ef72d300 feat(initialization): 优化镜像源选择界面和逻辑,修改后端端口为36163
- 将镜像源按类型分组展示,增加官方源和推荐标签
- 实现按速度和推荐程度排序镜像源的功能
- 添加镜像源描述信息,提高用户体验
- 优化后端服务和 WebSocket 连接端口
2025-08-29 01:25:14 +08:00
9f6c86dbbc refactor(initialization): 调整镜像源 2025-08-29 00:58:17 +08:00
d70112216f build(deps): 更新依赖并添加新的包 2025-08-28 23:17:31 +08:00
DLmaster361
e26ca8e81a fix: ws消息统一具有id字段 2025-08-27 23:31:19 +08:00
MoeSnowyFox
b5df09cfd2 🐛 保证应用单例运行 2025-08-27 20:29:39 +08:00
MoeSnowyFox
163cb78fc9 📝 更新打包方法 优化启动速度(目前没什么区别其实) 2025-08-27 20:29:08 +08:00
DLmaster361
bce9777eae fix: 修复ws断开时后端无法正常关闭 2025-08-27 18:35:50 +08:00
DLmaster361
af402330b2 fix: 修复处理数据文件升级时MAA默认配置文件缺失 2025-08-27 18:16:53 +08:00
DLmaster361
fbcc149849 refactor: 初步完成单一ws重构 2025-08-27 17:50:56 +08:00
DLmaster361
4c2a6407a1 feat: overview额外返回资源关信息 2025-08-27 15:37:56 +08:00
DLmaster361
331e0b55ee fix: 同步配置项数值范围上限为9999 2025-08-27 00:03:01 +08:00
DLmaster361
da0b379b69 feat: 添加更新器程序 2025-08-26 22:29:40 +08:00
DLmaster361
18a145b69b Merge branches 'feature/refactor-backend' and 'feature/refactor-backend' of github.com:DLmaster361/AUTO_MAA into feature/refactor-backend 2025-08-26 20:25:12 +08:00
464758073b refactor(initialization): 更新 ManualMode 组件的标题样式 2025-08-26 20:24:00 +08:00
DLmaster361
dea5f3db9a fix: 修复通用脚本根目录index 2025-08-26 20:17:14 +08:00
fdb724ae8b feat(scripts): 添加脚本配置功能 2025-08-26 20:08:43 +08:00
f7ac9daaac feat(core): 更新 MATERIALS_MAP 2025-08-26 18:32:53 +08:00
183e35ac97 refactor(initialization): 移除pip安装步骤 2025-08-26 16:45:49 +08:00
2f87d79713 feat(Scripts): 实现用户状态切换功能 2025-08-26 14:58:40 +08:00
3e0a9f3a94 refactor(Scripts): 重构脚本管理页面 2025-08-26 02:15:51 +08:00
DLmaster361
52fbd9225d fix: 初始化数据库文件夹 2025-08-25 15:27:52 +08:00
dc27127322 refactor(script): 重构脚本列表获取逻辑,优化用户数据加载方式 2025-08-25 13:25:58 +08:00
DLmaster361
92a4ed9529 fix: 总览中移除 SSReopen 关卡 2025-08-25 13:18:47 +08:00
DLmaster361
275e9e6bcc fix: 适配 MAA 剿灭关卡任务出错日志 2025-08-20 00:06:38 +08:00
DLmaster361
94ce37a948 feat: 添加配置文件自动升级v1.9过程 2025-08-18 17:29:00 +08:00
DLmaster361
998aaffd70 feat: 添加通用配置分享相关接口 2025-08-17 23:41:21 +08:00
DLmaster361
515367f61f feat: 添加基建配置文件设置接口 2025-08-17 15:54:54 +08:00
DLmaster361
8e2c6bb642 feat: 添加电源接口,适配小功能函数 2025-08-17 10:53:00 +08:00
08daef4dcd refactor(History): 重构历史记录页面布局和功能
- 新增左侧日期列表和右侧详情区域的布局结构
- 实现用户和记录选择功能
- 优化统计数据展示
- 添加详细日志刷新功能
- 调整样式和响应式设计
2025-08-16 00:22:37 +08:00
DLmaster361
1fdac22bea feat: 添加计划表下拉框接口 2025-08-15 23:07:18 +08:00
2f182519a5 refactor(queue): 重构队列数据获取逻辑
- 使用专门的定时项和队列项 API 获取数据,提高数据获取效率和准确性
- 优化数据处理逻辑,移除不必要的中间步骤
- 提高代码可读性和可维护性
2025-08-15 21:40:39 +08:00
b30073cb80 feat(router): 启动时强制访问初始化页面
- 新增 needInitLanding 标志位,用于控制是否需要跳转到初始化页面
- 修改路由守卫逻辑,确保应用启动时至少访问一次初始化页面
- 优化了开发环境下的路由处理流程
2025-08-15 20:32:11 +08:00
b3a15de00b feat(首页): 添加代理状态功能模块
- 新增代理状态卡片,展示代理用户信息和统计数据
- 实现代理数据的获取和格式化显示
- 添加用户代理次数、错误次数等统计信息
- 优化页面布局和样式,提升用户体验
2025-08-15 19:57:04 +08:00
DLmaster361
22a8cdb8d8 feat: 公告相关接口适配 2025-08-15 17:56:16 +08:00
DLmaster361
9acec97257 fix: 修正历史记录时间文本格式 2025-08-15 16:43:52 +08:00
32ddeef6f0 feat(History): 初步实现历史记录页面功能 2025-08-15 16:35:39 +08:00
a646578128 refactor(Scheduler): 重构调度台界面和逻辑 2025-08-15 16:08:56 +08:00
bf9b911cb2 refactor(Scheduler): 重构调度台界面和逻辑 2025-08-15 15:32:10 +08:00
DLmaster361
7ece8da1db fix: 将stage信息保存于Config文件中 2025-08-15 15:09:39 +08:00
a040dfc4ef refactor(Scheduler): 移除任务队列 2025-08-15 15:01:57 +08:00
7cc754f8b9 feat(api): 补充一下少提交的接口 2025-08-15 14:35:10 +08:00
71a5966700 feat(api): 新增多个API接口和相关模型 2025-08-15 14:28:22 +08:00
a738f102a6 style:格式化代码 2025-08-15 14:21:16 +08:00
9f849608db feat(scheduler): 实现调度台功能并优化任务执行界面
- 新增调度台标签页功能,支持多个调度台同时运行
- 优化任务执行界面布局,增加任务队列和用户队列显示
- 添加快速开始任务功能,可在当前调度台直接启动任务
- 实现任务完成后自动执行指定操作(如关机、睡眠等)
- 优化任务日志显示样式,增加信息过滤和分类
- 调整任务控制按钮位置,提高操作便利性
2025-08-15 01:24:40 +08:00
DLmaster361
7a253effa6 fix: 修复history目录不存在问题 2025-08-14 23:20:28 +08:00
DLmaster361
3c6c776828 feat: 总览接口添加用户代理情况信息 2025-08-14 17:58:50 +08:00
DLmaster361
2326cfcaa3 feat: 添加历史记录相关端口 2025-08-14 16:31:49 +08:00
DLmaster361
1f5cf3acff feat: 计划表添加端口字段说明 2025-08-14 01:22:51 +08:00
DLmaster361
829cac0f6a feat: 添加脚本相关接口文档,子配置数据获取接口独立 2025-08-14 00:56:38 +08:00
MoeSnowyFox
b18ad0fefa Merge remote-tracking branch 'origin/feature/refactor' into feature/refactor 2025-08-14 00:13:48 +08:00
MoeSnowyFox
2391e5b806 🧑‍💻 新增env.dev,用于区分环境类型, dev环境忽略路由守卫 2025-08-14 00:12:42 +08:00
68616fe75a feat(gitService): 优化 Git 克隆命令并设置管理员权限
- 在 Git 克隆命令中添加 --single-branch 和 --depth 参数,以提高克隆速度和效率
- 在 package.json 中设置 Windows 平台的请求执行级别为管理员
2025-08-13 23:55:38 +08:00
MoeSnowyFox
512667b850 Merge remote-tracking branch 'origin/feature/refactor' into feature/refactor 2025-08-13 23:38:42 +08:00
MoeSnowyFox
6765fbb36e 🎨 重写侧边栏 2025-08-13 23:36:54 +08:00
DLmaster361
9732a3e65f feat: 调度队列接口添加字段说明 2025-08-13 21:41:50 +08:00
2237dba3c5 feat(config): 重构镜像源配置管理 2025-08-13 21:02:49 +08:00
95126a85d8 feat: 允许http下载&添加了多个镜像源
- 在 AutoMode 组件中添加了多个 gh-proxy 镜像选项
- 在 BackendStep 组件中增加了新的镜像选择项
- 修改了 downloadService 中的下载逻辑,支持 http 和 https
- 更新了 gitService 和 pythonService 中的下载 URL
- 在 Home 组件中添加了动态问候语
2025-08-13 20:00:28 +08:00
DLmaster361
13e9d248e8 fix: rescourses改名res 2025-08-13 19:34:10 +08:00
9aeda23ade refactor(Scheduler): 优化任务输出和状态更新逻辑
- 改进输出内容的渲染和滚动逻辑
- 优化任务状态和日志的实时更新
- 调整任务对象的处理方式,使用 reactive 数组替换 ref 数组
- 优化 WebSocket 连接的管理
2025-08-13 16:43:12 +08:00
16ede80fd4 fix(Scheduler): 修复添加的弹窗选择器为空的问题 2025-08-13 16:22:51 +08:00
7d728cb3ae feat(api): 添加任务创建和执行功能 2025-08-13 15:54:10 +08:00
DLmaster361
bb84099072 fix: 尝试添加依赖 2025-08-13 00:31:49 +08:00
DLmaster361
bec82534f1 fix: 修改添加任务返回字段的键 2025-08-12 23:32:02 +08:00
DLmaster361
9c73dccd1e feat: 添加任务下拉列表接口 2025-08-12 22:48:19 +08:00
9fb25a2d33 feat(queue): 完成调度队列页面并添加脚本下拉框功能
- 重新设计了队列页面布局,使用表格代替原来的卡片布局
- 添加了脚本下拉框功能,支持选择已有的脚本
- 简化了队列项的展示内容,只显示脚本名称
- 优化了队列项的编辑和删除操作
- 新增了获取脚本下拉框信息的 API 接口
2025-08-12 22:26:15 +08:00
DLmaster361
175efdefde feat: 关卡号相关接口适配下拉框 2025-08-12 22:12:34 +08:00
DLmaster361
ff78f8eb27 feat: 添加脚本下拉框接口 2025-08-12 20:57:48 +08:00
DLmaster361
df7006dc1e feat: 优化全局配置接口 2025-08-12 20:30:20 +08:00
bc6ae5562e feat(i18n): 添加中文本地化支持并优化界面显示
- 在 App.vue 中添加中文本地化配置
- 在 main.ts 中配置 dayjs 中文本地化
- 优化 TimeSetManager 组件的样式和布局
- 调整部分组件属性以适应中文环境
2025-08-12 20:14:38 +08:00
DLmaster361
46ee99c5f2 feat: 关卡号添加本地缓存机制 2025-08-12 01:38:32 +08:00
c4dde028b2 feat(queue): 初步实现调度队列功能
- 添加队列列表获取、队列数据加载和保存功能
- 实现队列名称编辑、状态切换和删除功能
- 添加定时项和队列项的数据刷新和展示
- 优化队列界面样式,包括空状态、队列头部、配置区域等
2025-08-12 01:34:15 +08:00
DLmaster361
b543fedfaa Merge branch 'feature/refactor-backend' of github.com:DLmaster361/AUTO_MAA into feature/refactor-backend 2025-08-11 22:41:17 +08:00
DLmaster361
09371f0a5c feat: 通用调度适配 2025-08-11 22:40:42 +08:00
b4665309b9 feat(首页): 添加当期活动关卡功能
- 实现了当期活动关卡的展示功能,包括活动名称、时间、掉落物品等信息
- 添加了活动数据的异步加载和错误处理逻辑
- 优化了页面样式,增加了响应式布局支持
- 重构了部分代码,提高了可维护性和可读性
2025-08-11 16:28:51 +08:00
218784a8de refactor(config.py):优化侧故事关卡数据处理 2025-08-11 13:50:02 +08:00
16ab7fc176 feat(api): 添加任务调度相关接口和模型
- 新增 DispatchIn、TaskCreateIn 和 TaskCreateOut 模型
- 在 Service 类中添加任务调度相关接口:
- addOverviewApiInfoGetOverviewPost
- addTaskApiDispatchStartPost
- stopTaskApiDispatchStopPost
- 更新 index.ts,导出新增的模型和接口
- 优化导入路径,去除不必要的文件扩展名
2025-08-11 13:33:14 +08:00
85cd830046 feat(info): 添加获取活动关卡信息功能 2025-08-11 02:11:21 +08:00
76a6f8c33e feat(assets): 添加明日方舟材料图片 2025-08-11 01:57:31 +08:00
da9aa71c20 refactor(types): 合并 Electron API 类型定义
- 将 Electron API 类型定义从 vite-env.d.ts 移动到 electron.ts
- 更新 electron.ts 中的类型定义,增加了新的 API 方法
- 删除 vite-env.d.ts 文件中的 Electron API 类型定义
2025-08-11 00:26:37 +08:00
bf12f1f29a feat(initialization): 增加强制重新安装功能并优化环境检查逻辑
- 在 PythonStep、PipStep 和 GitStep 组件中添加强制重新安装功能
-优化环境检查逻辑,增加关键文件存在性检查
- 调整自动模式启动条件,确保关键文件存在
- 修复部分组件引用,增加必要的 ref
2025-08-10 23:19:44 +08:00
DLmaster361
91209ad9e2 feat: MAA任务可使用计划表信息 2025-08-10 22:30:10 +08:00
DLmaster361
6b729be34e feat: MAA完整功能上线 2025-08-10 21:45:35 +08:00
DLmaster361
409a7a2d03 feat: MAA调度初步上线 2025-08-09 22:17:19 +08:00
679c695700 feat(initialization): 实现后端服务自动启动功能
- 新增自动启动后端服务的功能,在第七步时自动尝试启动服务
- 修改手动启动按钮文案为"重新启动服务"
- 优化服务启动逻辑,统一处理自动启动和手动启动的流程
- 增加启动成功后的提示和自动进入主页的功能
2025-08-09 13:17:36 +08:00
DLmaster361
135555e3ea fix: 更正接口命名 2025-08-09 01:55:49 +08:00
DLmaster361
9a87a62353 feat: 没测过的MAA调度方案 2025-08-09 01:50:38 +08:00
1b94986e9a refactor(initialization): 调整自动模式时间和初始化页面样式
- 在 AutoMode.vue 中将自动模式完成时间从 5000000 毫秒调整为 500毫秒
- 在 Initialization.vue 中为初始化页面添加背景色和文本颜色样式
2025-08-09 01:16:26 +08:00
c8d3425293 feat(initialization): 新增管理员权限检查和自动启动功能
- 添加 AdminCheck 组件,用于提示用户需要管理员权限
- 实现 AutoMode 组件,用于自动启动后端服务
- 更新 DependenciesStep组件,移除部分镜像源
- 注释掉路由守卫代码,暂时不启用初始化页面强制跳转
2025-08-08 00:43:34 +08:00
0a49d45160 style(components): 移除速度徽章的背景颜色 2025-08-07 22:46:57 +08:00
0171c3ca4d feat(initialization): 新增配置文件操作和管理员权限检查
- 新增配置文件保存、加载和重置功能
- 添加管理员权限检查和重启为管理员的功能
- 实现 pip 包管理器安装功能
- 优化初始化流程,自动检测并安装依赖
2025-08-07 20:18:53 +08:00
e7f898f357 feat(initialization): 更新 Python 版本并优化初始化流程
-将 Python 版本从 3.13.0 更改为3.12.0
- 添加路由守卫,确保在生产环境中也能正确进入初始化页面
- 在初始化页面中增加服务启动相关逻辑
- 优化环境检查和启动服务的代码结构
- 调整后端服务启动方式,增加调试用的强制启动功能
2025-08-07 16:09:00 +08:00
DLmaster361
ba1fcd1f26 fix: main自动添加sys.path 2025-08-07 15:37:45 +08:00
DLmaster361
7941f5cafd fix: 将app设为包 2025-08-07 14:32:39 +08:00
6aca142696 refactor(initialization): 调整初始化流程并添加新功能
- 在 Git 克隆命令中添加了特定分支参数
- 在初始化界面添加了跳转到最后一部的按钮
- 调整了 Python 后端启动路径
2025-08-07 13:52:35 +08:00
f85a3024ef refactor(electron): 重构 Git 和后端代码获取逻辑
- 优化 Git 安装过程,使用临时目录解压后移动到目标位置
- 重新设计后端代码获取逻辑,支持跳过环境安装
- 添加跳转至首页按钮,便于开发测试
- 修复 Python 依赖安装路径问题
2025-08-07 01:37:52 +08:00
161dc478ae feat(log): 实现日志文件持久化和全局进度条
- 新增日志文件保存和加载功能
- 实现全局进度条组件
- 优化初始化界面布局
- 更新设置界面,增加系统日志查看按钮
2025-08-07 00:51:29 +08:00
ae151a9311 feat(initialization): 添加初始化页面和相关功能
- 新增初始化页面组件和路由
- 实现环境检查、Git下载、后端代码克隆等功能
- 添加下载服务和环境服务模块
- 更新类型定义,增加 Electron API 接口
2025-08-07 00:11:29 +08:00
DLmaster361
8cbd542a75 feat: 添加环境信息 2025-08-06 16:48:43 +08:00
18202045bf feat(Plans): 添加计划加载状态和空状态样式
- 在 Plans.vue 中添加 loading 相关逻辑,用于显示加载状态
- 实现空状态样式,优化无计划数据时的界面显示
- 调整模板结构,增加 loading-box 和空状态的 HTML 结构
- 优化 CSS 样式,为 loading 状态和空状态添加相应样式
2025-08-06 15:30:07 +08:00
89bf0cbad7 refactor(Plans): 重构计划页面布局和样式
- 调整了计划列表为空时的展示内容和样式
- 优化了计划选择器的位置和样式- 统一了计划内容区域的样式
- 移除了部分冗余的背景颜色设置
- 增加了新的空状态样式,提升用户体验
2025-08-06 15:22:49 +08:00
4ff041854c feat(plans): 实现计划管理功能
- 添加计划列表、创建计划、删除计划等功能
- 实现计划数据的加载和保存
- 优化空状态和面包屑样式
- 新增 usePlanApi 和 useUserApi 组合式函数
2025-08-06 15:07:09 +08:00
DLmaster361
b5c118eee5 fix: 修复大小写问题 2025-08-06 01:02:39 +08:00
be2c446906 fix: 修复按钮不能用的问题 2025-08-06 00:14:18 +08:00
5c57841cd9 refactor(electron): 优化应用启动和资源路径
- 更新应用图标路径,使用 src/assets 目录下的图标文件
- 重构应用启动逻辑,确保在不同环境下正确加载 HTML 文件
- 优化文件选择对话框的代码结构
- 在 vite 配置中添加 base 路径设置
2025-08-05 23:55:06 +08:00
40ca642c07 refactor(ScriptEdit): 更新 ScriptEdit 组件的按钮功能和图标
- 将取消按钮的图标从 CloseOutlined 改为 ArrowLeftOutlined
- 移除保存配置按钮
- 修改 Scripts 组件中添加脚本按钮的文案为"新建脚本"
2025-08-05 23:29:07 +08:00
DLmaster361
6898e548a5 feat: 载入各种服务 2025-08-05 22:50:07 +08:00
8c88e4e6a2 feat(ScriptEdit, UserEdit): 添加浮窗保存按钮
- 在 ScriptEdit 和 UserEdit 组件中添加浮窗保存按钮
- 按钮位于页面右侧,点击时触发表单提交
- 优化了用户编辑界面的样式
2025-08-05 22:48:55 +08:00
5ca4c5cc81 feat(UserEdit): 重构用户编辑页面并添加新功能
- 重新组织用户编辑页面的布局和结构
- 添加剩余天数、用户配置模式、剿灭代理等新功能
- 更新密码字段的描述,明确其仅用于储存
- 移除序列号字段
- 更新通知配置,支持多种通知方式
- 添加森空岛配置功能
2025-08-05 21:35:24 +08:00
eb0f2d521e feat(user): 添加用户编辑功能 2025-08-05 20:05:52 +08:00
d68e423768 feat(api): 添加 OpenAPI 客户端支持
- 新增 ApiError、ApiRequestOptions、ApiResult、CancelablePromise 等核心类
- 添加多种模型类型定义,如 PlanCreateIn、QueueGetOut、ScriptCreateOut 等
- 实现请求发送、错误处理、数据解析等核心功能
- 配置 axios 客户端并集成到请求流程中
- 优化路由配置,添加用户相关路由
- 更新脚本编辑界面文案,使用更通用的描述
2025-08-05 20:04:00 +08:00
DLmaster361
4ca7f9053f feat: 支持本地数据保存时加密 2025-08-05 18:56:44 +08:00
DLmaster361
d61b90baa4 feat: 添加信息获取API与主业务定时器 2025-08-05 16:09:48 +08:00
DLmaster361
4a9c9ab1f3 feat: 添加计划表相关API 2025-08-05 02:14:10 +08:00
DLmaster361
6fb4fa7683 feat: 添加排序相关API 2025-08-05 01:08:34 +08:00
DLmaster361
911eb60ae9 feat: 添加队列相关API 2025-08-05 00:46:22 +08:00
c116efd6f4 feat(settings): 新增设置页面和相关功能
- 添加设置页面组件和路由
- 实现设置数据的获取和更新逻辑
- 新增多个设置选项,包括功能设置、通知设置、更新设置等
- 优化设置页面样式和交互
2025-08-04 21:19:24 +08:00
DLmaster361
8e3c62a518 fix: 全局配置key错误 2025-08-04 21:13:57 +08:00
DLmaster361
16bf2c9494 feat: 添加用户相关API 2025-08-04 20:52:20 +08:00
9a9a4dad01 refactor(script): 重构脚本编辑页面并优化脚本配置功能 2025-08-04 19:51:56 +08:00
DLmaster361
07b5f5c00b Merge branch 'feature/refactor-backend' of github.com:DLmaster361/AUTO_MAA into feature/refactor-backend 2025-08-04 19:44:53 +08:00
DLmaster361
b60225cb74 feat: 添加全局配置相关API 2025-08-04 19:44:48 +08:00
MoeSnowyFox
250b2e9509 🐛 修正dev.md 2025-08-04 19:18:00 +08:00
f68c1c95eb refactor(api): 修改api调用url 2025-08-04 16:31:26 +08:00
DLmaster361
e71a518b49 feat: 修改脚本配置API 2025-08-04 16:13:00 +08:00
5531f6e87a feat(script): 添加脚本删除功能并优化脚本编辑界面
- 在 ScriptEdit.vue 中添加删除脚本 API 响应类型
- 在 Scripts.vue 和 ScriptTable.vue 中移除冗余样式
- 在 tsconfig.json 中添加路径别名配置
- 重构 useScriptApi.ts 中的 deleteScript 函数,实现真正的脚本删除
2025-08-04 15:49:00 +08:00
DLmaster361
a8ec29d2ed feat: 适配脚本删除API 2025-08-04 15:23:41 +08:00
19aab99398 refactor(eslint): 重构 ESLint 配置并优化组件样式
- 将 ESLint 配置从 .eslintrc.js 迁移到 eslint.config.js
- 优化 Scripts.vue 和 ScriptTable.vue 组件的样式
- 移除了不必要的模拟数据
- 调整了按钮样式和布局
2025-08-04 15:23:20 +08:00
0b1ed48471 feat(script): 添加脚本管理页面和相关功能
- 实现了脚本管理页面的基本布局和样式
- 添加了脚本列表加载、添加、编辑和删除功能- 集成了文件夹和文件选择对话框
- 优化了主题模式和颜色的动态切换
- 新增了多个脚本相关类型定义
2025-08-04 14:42:31 +08:00
DLmaster361
0b9c6320eb fix: 修正查询方法 2025-08-04 14:09:01 +08:00
DLmaster361
03b20787b3 fix: 移除无用方法 2025-08-04 13:56:09 +08:00
DLmaster361
9f8f411c33 feat: 实现脚本查询 2025-08-04 13:50:31 +08:00
DLmaster361
76438459f7 feat: 接入logger 2025-08-04 10:46:23 +08:00
92ea6026f8 feat(api): 添加跨域资源共享 (CORS) 中间件 2025-08-04 02:22:29 +08:00
DLmaster361
9031ea6b3f feat: 配置基类添加异步 2025-08-04 01:36:26 +08:00
DLmaster361
b576440612 feat: 适配添加脚本功能 2025-08-04 01:12:06 +08:00
DLmaster361
a4891131fc feat(core): 完成配置类初始化任务 2025-08-04 00:01:34 +08:00
DLmaster361
5f57ce54aa feat: 创建配置类模型 2025-08-03 17:42:18 +08:00
MoeSnowyFox
f1859a5877 ♻️重构, 实现基础框架-日志 AUTO_MAA配置 用户配置基类 2025-08-03 03:43:57 +08:00
94d038d563 feat: 修改网页(应用程序)标题为 AUTO_MAA 2025-08-03 02:00:58 +08:00
ffae1e583b refactor(layout): 调整侧边栏宽度并优化开发者工具
- 调整侧边栏展开/折叠宽度,适应更多显示器
- 优化开发者工具,在新窗口中打开,解决部分显示器兼容性问题
- 添加浅色模式下侧边栏和菜单的背景色样式
2025-08-03 01:58:57 +08:00
8916cbd097 feat(layout): 优化侧边栏布局和内容区域滚动
-固定侧边栏高度为100vh,设置为固定定位
- 优化内容区域样式,设置过渡效果和滚动条- 隐藏菜单和内容区域的滚动条
- 调整底部菜单样式,注释掉部分代码
2025-08-03 01:02:14 +08:00
790a75ac87 feat(settings): 重构设置页面
- 重新设计了设置页面布局,增加了标签页导航
- 添加了通知设置、更新设置和高级设置等新功能
- 优化了主题模式和主题色选择功能
2025-08-03 00:23:28 +08:00
d4f8165b87 feat(frontend): 实现 AUTO_MAA 前端基础结构
- 创建 Vue 3 + TypeScript 项目
- 添加 Electron 支持
- 实现基本页面布局和路由- 添加主题切换功能
- 创建设置页面
- 添加开发者工具支持
2025-08-02 23:10:22 +08:00
DLmaster361
8948d0fb18 feat: 发布v4.4.1 2025-08-01 11:02:08 +08:00
DLmaster361
908da0bc47 fix: 修复计划表未能按照鹰历获取关卡号的问题 2025-08-01 09:35:54 +08:00
DLmaster361
aae17208b0 fix: 修正部分文案 2025-08-01 01:09:10 +08:00
DLmaster361
cbd8918c61 feat(core): 启动时支持直接运行复数调度队列 2025-07-31 23:18:36 +08:00
DLmaster361
f2b4f9e8fc ci: fix ci 2025-07-31 22:07:21 +08:00
DLmaster361
a2a11647bb fix: ci 2025-07-31 21:58:53 +08:00
DLmaster361
1d9c275b61 ci: fix 2025-07-31 21:53:44 +08:00
DLmaster361
52468928c6 feat: 新增 Go_Updater 独立更新器 2025-07-31 20:47:49 +08:00
DLmaster361
3d9471779a Merge branch 'ClozyA_GoUpdater_dev' into dev 2025-07-31 20:42:52 +08:00
DLmaster361
155c4b00d5 ci: 适配Go_Updater 2025-07-31 20:42:36 +08:00
DLmaster361
f4b45e9eae feat(general): 通用脚本启动附加命令添加额外的语法以适应UI可执行文件与任务可执行文件不同的情况 2025-07-31 20:01:32 +08:00
DLmaster361
d836b58b2d feat: 新增完成任务后自动复原脚本配置 2025-07-31 17:06:08 +08:00
DLmaster361
4fc747f1c6 feat(maa): 适配 MAA 长期开放剿灭关卡 #58 2025-07-31 16:13:35 +08:00
DLmaster361
b27f2c43ae feat: MAA代理时更新改为强制开启 2025-07-31 13:50:19 +08:00
DLmaster361
c5f947e14a fix: 优化静默进程标记逻辑 2025-07-31 13:39:38 +08:00
DLmaster361
67c41ab3ee chore(core): 优化调度队列配置逻辑 2025-07-31 01:01:30 +08:00
DLmaster
9f330e2245 Merge pull request #62 from Zrief:dev
修复依赖缺失
2025-07-31 00:49:39 +08:00
Zrief
d785262312 依赖相关问题
在 app/core/task_manager.py 文件中,第 31 行使用了 packaging 模块。其并非标准库,因此需要确保它被包含在项目依赖中。
2025-07-31 00:44:07 +08:00
DLmaster361
27faaabcf2 fix: 修复脚本下载器卡初始化 2025-07-27 23:30:34 +08:00
226d68cb1c feat(api): 修复beta版本的更新检查 2025-07-27 14:12:54 +08:00
DLmaster361
e776cc2319 fix: 修复一般更新方案卡初始化 2025-07-26 16:23:22 +08:00
DLmaster361
3cdb1e511d fix: 修正部分格式 2025-07-26 13:11:23 +08:00
a4f867665f feat(power): 添加强制关机功能并优化关机流程 2025-07-26 00:27:41 +08:00
DLmaster361
6b0583b139 fix: 移除部分调试代码 2025-07-25 20:42:50 +08:00
2aad0c65c2 chore: 添加无更新可用时的延迟退出功能 2025-07-22 23:03:23 +08:00
6b646378b6 refactor(updater): 重构 Go 版本更新器
- 更新项目名称为 AUTO_MAA_Go_Updater
- 重构代码结构,优化函数命名和逻辑
- 移除 CDK 相关的冗余代码
- 调整版本号为 git commit hash
- 更新构建配置和脚本
- 优化 API 客户端实现
2025-07-22 21:51:58 +08:00
747ad6387b feat(config): 添加配置管理功能 2025-07-20 18:12:35 +08:00
228e66315c feat(Go_Updater): 添加全新 Go 语言实现的自动更新器
- 新增多个源文件和目录,包括 app.rc、assets、build 脚本等
- 实现了与 MirrorChyan API 交互的客户端逻辑
- 添加了版本检查、更新检测和下载 URL 生成等功能
- 嵌入了配置模板和资源文件系统
- 提供了完整的构建和发布流程
2025-07-20 16:30:14 +08:00
DLmaster361
97c283797a fix: 移除崩溃弹窗机制 2025-07-19 22:23:59 +08:00
DLmaster361
eb1fade6f5 feat: AUTO_MAA 配置分享中心上线 2025-07-19 21:38:38 +08:00
DLmaster361
8d6071f794 fix: 修复 QTimer.singleShot 参数问题 2025-07-19 17:08:57 +08:00
DLmaster361
8a109e34f8 refactor(module): 重构日志读取兜底机制 2025-07-19 10:27:19 +08:00
DLmaster361
fd72d72692 fix(module): 日志读取添加兜底机制 2025-07-19 10:13:48 +08:00
DLmaster361
63ffacff96 fix(ui): SpinBox和TimeEdit组件忽视滚轮事件 2025-07-18 21:59:18 +08:00
DLmaster361
1b4bb6fccc feat(general): 通用脚本支持在选定的时机自动更新配置文件 2025-07-18 21:48:39 +08:00
DLmaster361
9b492b5e0d feat(core): 重构日志记录,载入更多日志记录项 2025-07-18 18:12:47 +08:00
DLmaster361
8427bd9f6b Merge branch 'inno_dev' into dev 2025-07-16 20:13:45 +08:00
DLmaster361
2c915161d5 Merge commit '75b06ca770ebcee68bf05ab95f069c6e8fd0cae8' into dev 2025-07-15 18:15:48 +08:00
DLmaster361
75b06ca770 feat(general): 通用配置模式接入日志系统 2025-07-15 18:15:26 +08:00
DLmaster361
c3468a3387 Merge branch 'generic_dev' into dev 2025-07-15 16:20:26 +08:00
DLmaster361
a2f4adb647 fix: 适配 MAA 任务及基建设施日志翻译 2025-07-15 16:20:09 +08:00
DLmaster361
403f69df8b refactor(ui): 重构历史记录保存与载入逻辑 2025-07-15 16:08:11 +08:00
DLmaster361
12cf10f97a fix((ui): 修复MAA用户仪表盘备选1字段错误 2025-07-14 19:12:59 +08:00
DLmaster361
6084befe2c Merge branch 'dev' into generic_dev 2025-07-13 23:25:02 +08:00
DLmaster361
1aa99ea613 Merge branch 'network_dev' into dev 2025-07-13 23:00:59 +08:00
DLmaster361
d539c0f808 fix(core): 信任系统证书,并添加网络代理地址配置项 #50 2025-07-13 22:50:04 +08:00
DLmaster361
bc509806fb Merge branch 'generic_dev' into dev 2025-07-12 23:11:35 +08:00
DLmaster361
c52820550f feat(ui): 添加导入导出通用配置功能 2025-07-12 21:54:40 +08:00
DLmaster361
98b30f90a1 Merge commit '4efbafc174504bd2fbd5b22075e3b5429406691b' into generic_dev 2025-07-12 19:41:10 +08:00
DLmaster361
4efbafc174 fix(system): 修复开机自启相关功能 2025-07-12 16:08:07 +08:00
DLmaster361
6d3fda50d3 feat(security): 添加重置管理密钥功能 2025-07-12 01:27:27 +08:00
DLmaster361
70b936012f Merge commit '54917fbe6de3ca5ac4a07bc88cdf0178a23f88dc' into dev 2025-07-11 18:42:40 +08:00
DLmaster361
54917fbe6d fix(general): 修复无成功日志时的脚本判定逻辑 2025-07-11 18:38:58 +08:00
DLmaster361
abeb9f054d fix(maa): 适配 MAA 备选关卡字段修改 2025-07-11 14:48:23 +08:00
DLmaster361
c6d6c5fb2a Merge branch 'generic_dev' into dev 2025-07-10 17:35:22 +08:00
DLmaster361
5b0d7f0012 feat(ui): 修改版本号到v4.4
- 进一步适配三月七相关配置项
2025-07-10 17:34:41 +08:00
DLmaster361
d9043aab0a fix(ui): 适配 Mirror 酱 平台下载策略调整 2025-07-10 13:29:17 +08:00
DLmaster361
b9281b68ab feat(utils): 安装包适配管理员权限 2025-07-10 04:53:32 +08:00
DLmaster361
5c6a20be4e Merge branch 'generic_dev' into dev 2025-07-10 02:30:02 +08:00
DLmaster361
1c0a65957d feat(core): 初步完成通用调度模块 2025-07-10 02:29:08 +08:00
DLmaster361
7c315624b1 Merge commit '0572caa528deb72bc97c42e0b169292823b1e8a8' into dev 2025-06-17 19:55:55 +08:00
DLmaster361
0572caa528 fix: 固定certifi版本号 2025-06-17 03:18:55 +08:00
DLmaster361
4233040585 fix: 尝试固定certifi版本号 2025-06-17 02:46:27 +08:00
DLmaster361
c27dc8e380 fix: 移除无用依赖项 2025-06-17 02:00:32 +08:00
DLmaster361
e746756e56 ci: 临时固定Nuitka打包版本号 2025-06-17 01:14:29 +08:00
DLmaster361
1829d1cd0b fix(ui): 修复删除计划表引发的错误 2025-06-17 00:34:07 +08:00
DLmaster361
fb979e5639 docs: 补充代码签名策略 2025-06-16 21:38:25 +08:00
DLmaster361
e7d0a85ad5 ci: 修正project-slug 2025-06-16 21:24:05 +08:00
DLmaster361
a384711327 Merge branch 'dev' 2025-06-16 14:33:49 +08:00
DLmaster361
3fd4778a48 feat(maa): 适配 MAA 无Default配置情况 2025-06-16 14:26:29 +08:00
DLmaster361
4841dc09b3 refactor(res): 更新软件主页用图 2025-06-14 17:20:03 +08:00
DLmaster361
b3aa4fc776 fix(maa): 修复森空岛签到信息特定情况下不弹出通知的问题 2025-06-13 10:02:43 +08:00
DLmaster361
a9b3b8b6f4 Merge branch 'Clozy_dev' into dev 2025-06-11 23:05:59 +08:00
DLmaster361
56ef196695 refactor(models/services): 简化测试用图 2025-06-11 23:05:46 +08:00
242238d341 refactor(models/services): 优化代码结构和可读性
-移除了不必要的变量声明
-简化了图像路径的构建方式
- 统一了代码格式
2025-06-11 22:51:26 +08:00
f66f6d38fe feat(notification): 用户单独通知六星喜报 2025-06-11 22:31:51 +08:00
d58077f58b fix(app): 修复企业微信机器人图片推送异常
- 移除了不必要的变量 final_image_path
-直接使用 image_path 进行图片存在性检查
- 更新了图片 base64 和 md5计算的逻辑
2025-06-11 22:26:44 +08:00
4d4d6dbedf refactor(app): 重构图片压缩功能并优化类型注解
- 使用 Path 对象替换字符串表示文件路径,提高代码可读性和功能
- 优化类型注解,包括函数参数和返回值- 重构 ImageUtils.compress_image_if_needed 方法,简化逻辑并提高性能
- 更新 notification.py 中使用该方法的代码,直接返回压缩后的 Path 对象
2025-06-11 22:14:09 +08:00
f60b276916 refactor(notify): 重构企业微信群机器人图片推送功能
-将图片压缩、存在性检查、Base64编码和MD5计算移至 CompanyWebHookBotPushImage 方法内部
- 优化错误处理和日志记录
- 简化调用接口,提高代码可读性和维护性
2025-06-11 20:54:45 +08:00
87857fd499 feat(notification): 新增图片压缩处理
- 新增 ImageUtils.compress_image_if_needed 方法,用于压缩图片大小
- 在 MAA.py 和 notification.py 中集成图片压缩功能
- 添加对不同图片格式(JPEG、PNG)的压缩支持
- 优化图片路径处理,确保压缩后图片正确发送
- 更新 requirements.txt,添加 pillow 依赖
2025-06-11 19:50:58 +08:00
3c371cd079 feat(notification): 企业微信群机器人支持图片推送
- 新增 ImageUtils 类,提供图像处理相关工具方法
- 在 MAA.py 中集成 ImageUtils,用于获取和处理通知图片
- 在 notification.py 中实现 CompanyWebHookBotPushImage 方法,支持企业微信群机器人推送图片
- 修改测试通知方法,增加图片推送测试
2025-06-11 17:36:11 +08:00
DLmaster361
428b849bcc fix(maa): 静默模式控制时段延长至模拟器完成启动的10s后 2025-06-11 01:13:57 +08:00
DLmaster361
85f3b4f607 Merge branch 'dev' 2025-06-10 18:49:00 +08:00
DLmaster361
916396f855 refactor: 使用 keyboard 模块替代 pyautogui 模块 2025-06-10 18:48:41 +08:00
DLmaster361
211c8d2b04 Merge branch 'dev' 2025-06-10 14:10:01 +08:00
DLmaster361
92e274d3fd ci: 移除pyscreeze 2025-06-10 14:09:40 +08:00
DLmaster361
d511ea48d5 Merge branch 'main' into dev 2025-06-09 23:45:03 +08:00
DLmaster361
1aa4da1adf feat: 支持使用命令行调用 2025-06-09 23:43:36 +08:00
DLmaster361
0e8b6b0b6b feat: 添加用户守则 2025-06-08 23:41:18 +08:00
DLmaster361
1a2c1b976f fix(maa): 更新动作执行后移除相应标记 2025-06-07 15:59:06 +08:00
DLmaster361
1cc242fa51 feat: 优化下载器测速中止条件 2025-06-06 22:38:37 +08:00
DLmaster361
18dfdba15d ci: 测试完整工作流 2025-06-06 00:09:01 +08:00
DLmaster361
b04ac4eec6 ci: 使用预设配置 2025-06-05 23:50:56 +08:00
DLmaster361
c009f0c891 ci: 测试证书注册 2025-06-05 23:39:09 +08:00
DLmaster361
d2dc0bd295 ci: 上传测试之二 2025-06-05 23:28:15 +08:00
DLmaster361
ddbb5b7f19 ci: 名字作出区分 2025-06-05 23:25:48 +08:00
DLmaster361
954c25090b ci: 测试上传工作 2025-06-05 23:25:08 +08:00
DLmaster361
0b6cc59de1 ci: 构建部分测试流程 2025-06-05 22:38:24 +08:00
DLmaster361
2271b5741d ci: 移除不支持的参数 2025-06-05 22:08:41 +08:00
DLmaster361
8a438b041f ci: 修正signpath证书参数 2025-06-05 21:09:03 +08:00
DLmaster361
dd92fcc4d8 ci: 添加证书测试工作流 2025-06-05 20:02:17 +08:00
DLmaster361
8f66ca0e16 Merge branch 'dev' 2025-06-05 19:10:49 +08:00
DLmaster361
895ba1d24a feat(res): 公招喜报模板优化 2025-06-04 11:21:34 +08:00
e49b807bef feat(ui): 完善森空岛签到功能的提示信息 2025-06-02 14:26:25 +08:00
DLmaster361
73c15b5e93 refactor(skland): 森空岛签到功能拆分独立 2025-06-02 14:05:31 +08:00
DLmaster361
e505ea8c51 feat(maa): 森空岛签到功能上线 2025-06-02 02:35:01 +08:00
DLmaster361
21e7df7c3e Merge branch 'dev' 2025-06-01 20:04:16 +08:00
DLmaster361
2d72ca66a4 fix(core): 修复网络模块子线程未及时销毁导致的程序崩溃 2025-06-01 19:52:22 +08:00
DLmaster361
4725a30165 fix(ui): 修复语音包禁忌二重奏 2025-06-01 04:31:29 +08:00
DLmaster361
f3c977f1b3 Merge branch 'main' into dev 2025-06-01 03:18:39 +08:00
DLmaster361
9a0e7265c6 feat(core): 语音功能上线 2025-06-01 03:16:56 +08:00
DLmaster361
3f8e2fbe6b Merge branch 'dev' 2025-05-31 13:37:06 +08:00
DLmaster361
590b13e916 ci: 移除测试打包流程 2025-05-31 13:36:09 +08:00
DLmaster361
0f6aee56e5 ci: 修正测试工作流名称 2025-05-31 11:21:13 +08:00
DLmaster361
daf18e7295 Merge branch 'dev' 2025-05-31 11:20:04 +08:00
DLmaster361
9bcc87f663 ci: 添加引入打包action的测试工作流 2025-05-31 11:19:36 +08:00
DLmaster361
e7205ce0aa fix(services): 非UI组件转为QObject类 2025-05-30 21:06:39 +08:00
DLmaster361
e3c4b2edc8 fix(core): 网络模块支持并发请求 2025-05-30 20:30:23 +08:00
DLmaster361
222a3b35a2 fix(maa): 修复ADB与模拟器相关日志信息报错时不显示 2025-05-29 21:06:09 +08:00
DLmaster361
cd5dfd56b2 Merge branch 'dev' 2025-05-28 23:13:15 +08:00
DLmaster361
7d5c6b8222 docs: 添加DeepWiki徽章 2025-05-28 23:12:56 +08:00
DLmaster361
4dbf4736e4 Merge branch 'dev' 2025-05-28 21:18:24 +08:00
DLmaster361
d50504181e feat(ui): 吐司通知在主窗口隐藏时不再弹出 2025-05-28 21:17:14 +08:00
DLmaster361
c7e94dfcd1 Merge branch 'DLMS_dev' into dev 2025-05-27 18:17:29 +08:00
DLmaster361
a752b67ca1 feat(ui): UI界面添加自动日常代理任务序列设置项 2025-05-26 16:05:46 +08:00
DLmaster361
078736337d fix: 修复雷电模拟器静默模式无法正常识别模拟器是否隐藏相关问题 2025-05-25 23:31:37 +08:00
DLmaster361
de1058a28c feat(core): 计划表功能上线 2025-05-25 21:12:22 +08:00
DLmaster361
740797a689 Merge branch 'dev' into DLMS_dev 2025-05-24 19:56:16 +08:00
DLmaster361
26328920a2 chore(ui): 输入文本框适配文本插入操作 2025-05-24 19:55:49 +08:00
DLmaster361
9c447bbdf9 Merge branch 'ClozyA_dev' into dev 2025-05-24 18:34:03 +08:00
DLmaster361
fac85a889f fix(ui): 简单优化用户通知显示效果 2025-05-24 18:33:46 +08:00
DLmaster361
f5d898c89e fix(maa): 将通知判定过程全部移入push_notification 2025-05-23 22:59:09 +08:00
974a4b634a feat(ui): 优化敏感信息显示逻辑 2025-05-23 15:19:35 +08:00
3127c83603 feat(notification): 增加用户单独通知功能 2025-05-23 15:01:26 +08:00
DLmaster361
8d69e43f72 feat(ui): 初步添加周计划表 2025-05-22 23:20:38 +08:00
DLmaster361
86df9e7a50 chore(ui): 优化二级菜单显示效果 2025-05-21 01:00:38 +08:00
59ff9bf818 refactor(notification): 重构通知模块 2025-05-20 02:11:54 +08:00
DLmaster361
1641e32e3d feat(ui): 完成完整用户通知子菜单设计 2025-05-19 17:31:50 +08:00
DLmaster361
a482087abd feat(ui): 初步完成用户通知相关界面 2025-05-19 16:54:14 +08:00
bc5b15cec2 feat(notification): 优化用户通知服务 2025-05-19 01:10:20 +08:00
3787c25a77 Merge branch 'dev' into ClozyA_dev 2025-05-19 00:18:37 +08:00
DLmaster361
0b06b499e4 fix(models): 修复雷电ADB端口号相关问题 2025-05-18 23:35:35 +08:00
DLmaster361
04079dd57b Merge branch 'dev' of github.com:DLmaster361/AUTO_MAA into dev 2025-05-18 13:54:19 +08:00
DLmaster361
34ac0c5ab3 fix: 主动关闭剩余理智选项 2025-05-18 13:54:14 +08:00
0d904b229e feat(core): 初步完成新增用户单独通知功能 2025-05-18 01:57:48 +08:00
c0f887ff9d refactor(notification): 更新任务报告标题格式,增加日期前缀 2025-05-18 00:17:06 +08:00
DLmaster361
cf95075d01 fix(ui): 修复窗口最大化功能异常 2025-05-17 22:26:41 +08:00
DLmaster361
d78a764d87 fix: 修正搜索ADB时的等待方法 2025-05-17 00:07:34 +08:00
DLmaster
a368f4b722 feat(ui): 用户仪表盘支持直接控制用户状态
优化仪表盘排版
2025-05-16 23:27:53 +08:00
DLmaster361
803fe4568f chore(ui): 优化实现方法 2025-05-16 23:23:02 +08:00
DLmaster361
1162d5dcc1 feat(ui): 添加对剩余天数为0的展示 2025-05-16 22:50:54 +08:00
Zrief
aa83058e39 Update member_manager.py 2025-05-16 21:56:20 +08:00
Zrief
061f205224 Merge branch 'DLmaster361:dev' into dev 2025-05-16 21:55:16 +08:00
Zrief
5d966f98df 修改逻辑不完备的地方 2025-05-16 21:53:03 +08:00
DLmaster361
0037914db8 Merge branch 'DLMS_dev' into dev 2025-05-16 20:41:04 +08:00
DLmaster361
13d0115475 feat(core): 添加ADB端口号宽幅适配能力 2025-05-16 20:09:29 +08:00
Zrief
5bdb5ad2bf 优化脚本仪表盘排版
可以直接在仪表盘里面开关用户状态了
2025-05-15 21:20:30 +08:00
a5d733c8df feat(notification): 为自动代理统计报告添加日期前缀 2025-05-15 16:39:56 +08:00
DLmaster361
0b038e2997 Merge branch 'main' into dev 2025-05-12 14:54:17 +08:00
DLmaster361
5e46040db6 feat: 日志分析忽略MAA超时提示 2025-05-12 14:53:57 +08:00
DLmaster361
f2b04dd0f6 Merge branch 'dev' 2025-05-11 23:28:18 +08:00
DLmaster361
2177c1b40e fix(models): 适配MAAv5.16.3的ADB报错信息更改 2025-05-11 23:26:42 +08:00
DLmaster361
d1f4cffe8f feat(ui): 主调度台添加仅一次电源任务,UI样式优化 2025-05-10 23:15:54 +08:00
DLmaster361
74ce441b90 feat(core): 电源相关选项改为所有任务完成后生效 2025-05-09 21:52:18 +08:00
DLmaster361
5893aa2426 feat(ui): 自定义基建显示配置名称 2025-05-09 19:54:07 +08:00
DLmaster361
cb7e7bf9d4 Merge branch 'dev' 2025-05-09 14:54:29 +08:00
DLmaster361
fbfdc6aa12 feat: 优化更新方式 2025-05-09 14:54:09 +08:00
DLmaster361
e7b6743e10 test: 移除测试ci 2025-05-06 17:43:23 +08:00
DLmaster361
ff4283e917 Merge branch 'dev' 2025-05-06 17:42:42 +08:00
DLmaster361
890886d62d fix(ci): 适配新下载站模式 2025-05-06 17:42:24 +08:00
DLmaster361
fd75dda2b1 feat(ui): 适配MAA连战次数AUTO模式 2025-05-06 13:57:49 +08:00
DLmaster361
f22c1aeae3 test(ci): 补充步骤 2025-05-06 13:07:56 +08:00
DLmaster361
d68d49a469 test(ci): 更正环境 2025-05-06 13:00:04 +08:00
DLmaster361
1900d4eaf5 test(ci): 测试ssh部分 2025-05-06 12:57:55 +08:00
DLmaster361
02833209d5 Merge branch 'dev' 2025-05-06 00:18:44 +08:00
2058c0218c ci: 更新下载服务器配置 2025-05-06 00:09:14 +08:00
DLmaster361
8896e723eb fix: 补充版本号 2025-05-05 20:14:25 +08:00
DLmaster361
edcc614833 Merge commit '23fe1ff0beb234eb7ebbc07b189782b7fb84d5e5' 2025-05-05 20:13:22 +08:00
DLmaster361
23fe1ff0be feat: release中添加安装程序 2025-05-05 20:12:59 +08:00
DLmaster361
19d1dc9f28 fix: 修复详细模式下非定时自定义基建无法正常换班的问题 2025-05-05 15:59:07 +08:00
DLmaster361
24b93cfcad feat: 下载器支持调用Mirror酱 2025-05-04 23:43:04 +08:00
DLmaster361
d3298fac8a fix: 移除serverchan_sdk 2025-05-04 17:01:36 +08:00
DLmaster361
fba5395bf0 Merge branch 'dev' 2025-05-04 16:44:46 +08:00
DLmaster361
2c4508ee16 fix: 修复版本号错误 2025-05-04 16:43:49 +08:00
d239443555 chore(release): 发版 4.3.6.2 2025-05-04 16:36:36 +08:00
e45ad08fab refactor(notification): 重构 Server酱 推送服务 2025-05-04 16:25:46 +08:00
ddf5d26c4b refactor(notification): 重构 Server酱 推送服务 2025-05-04 16:12:45 +08:00
DLmaster361
ce74dcf912 Merge branch 'dev' of github.com:DLmaster361/AUTO_MAA into dev 2025-05-04 15:18:31 +08:00
DLmaster361
41412e1ef4 feat(ui): 新增无人值守模式 2025-05-04 15:18:26 +08:00
雪影
1395d48cd0 头部信息居中 (#43)
Co-authored-by: DLmaster361 <DLmaster_361@163.com>
2025-05-04 13:57:56 +08:00
DLmaster361
418c3d4742 fix(ui): 修复隐藏到托盘时,托盘无法退出主程序的问题 2025-05-04 11:12:34 +08:00
DLmaster361
17ec962a22 fix(ui): 修复软件窗口相关问题
- 修复软件窗口最大化异常问题
- 修复异常操作导致窗口离开屏幕后难以复原的问题
- 修正剩余理智关卡文案
- 主窗口显示版本号
2025-05-04 03:14:14 +08:00
DLmaster361
989ee73549 feat(maa): 单次自动代理任务中,已完成的子任务在重复执行时不再启用 2025-05-02 11:36:40 +08:00
DLmaster361
7e452e1253 Merge branch 'dev' 2025-05-02 00:40:55 +08:00
DLmaster361
5bdb5c8025 fix: 详细配置模式下更新也由AUTO统筹配置 2025-05-02 00:39:58 +08:00
DLmaster361
924a5fea0b feat: 适配6周年游戏变动的优化
- 用户设置中新增连战次数与剩余理智关卡两项配置项
- 支持自动代理时更新MAA
- 移除增效任务
2025-05-01 21:31:09 +08:00
DLmaster361
b51a57a6ee fix: 修复无法建立网络连接时软件卡死问题 2025-04-29 09:40:11 +08:00
DLmaster361
4079188881 fix: 脚本实例任务完成后即时更新状态信息 2025-04-28 20:41:08 +08:00
DLmaster361
174163e305 feat: 模拟器路径适配快捷方式 2025-04-28 18:35:56 +08:00
DLmaster361
0886439685 Merge branch 'dev' 2025-04-27 01:00:18 +08:00
DLmaster361
34bf5a4fe8 fix: 同步到MAAv5.15.4的tasks目录结构变化 2025-04-27 00:59:46 +08:00
DLmaster361
e6a97f2b17 feat(ui): 主调度台支持直接启动多开调度台 2025-04-26 22:40:47 +08:00
DLmaster361
fecff625a3 feat: 新增MAA稳定版更新提醒 2025-04-26 11:28:14 +08:00
DLmaster361
6f540036a0 feat(ui): 历史记录功能升级 2025-04-25 22:47:00 +08:00
DLmaster361
86d72aec39 feat: 历史记录页面添加选中最近一月选中最近一周选项 2025-04-23 15:04:36 +08:00
DLmaster361
39876832f3 fix: 历史记录加载方法优化 2025-04-21 19:21:27 +08:00
DLmaster361
f3af6ddbbc fix: 修复更新时主程序退出不彻底与ADB路径与模拟器路径相关问题 2025-04-20 16:40:14 +08:00
DLmaster361
ba7299e20c fix: 同步核心版本号 2025-04-20 11:14:21 +08:00
DLmaster361
5db9d934b2 fix: 修复更新器使用mirrorc下载时删除已弃用的文件卡死 2025-04-20 11:07:41 +08:00
DLmaster361
5c8eebf12c fix: 清理debug时的print 2025-04-20 00:18:31 +08:00
DLmaster361
e725f6d2b2 Merge branch 'main' into dev 2025-04-20 00:05:17 +08:00
DLmaster361
494b655156 Merge branch 'dev' 2025-04-20 00:02:42 +08:00
DLmaster361
2940f2557c Merge branch 'DLMS_dev' into dev 2025-04-20 00:02:25 +08:00
DLmaster361
5e4660670f chore: 性能优化
- 调度队列历史记录归入配置类管理
- 添加.gitignore
- 工作流删除冗余部分
- 自动代理与人工排查结束后MAA恢复到全局配置 #40
- 网络相关操作由子线程执行
2025-04-20 00:01:49 +08:00
e8d592ae76 refactor(app): 优化关卡掉落物品正则匹配 2025-04-16 16:58:22 +08:00
DLmaster361
97ea51df59 docs: README链接修复 2025-04-16 11:23:24 +08:00
DLmaster361
986061dc97 Merge branch 'DLMS_dev' into dev 2025-04-15 22:34:52 +08:00
DLmaster361
fe1910d16f fix: 修正部分配置项文案 2025-04-15 22:34:37 +08:00
DLmaster361
63cb1aaa74 feat: 自动代理流程优化 2025-04-15 22:15:59 +08:00
49ebd50077 refactor(ui): 优化模拟器老板键设置卡片的提示文本 2025-04-14 16:04:53 +08:00
DLmaster361
4a6f874210 fix: request 添加超时限制 2025-04-14 15:03:29 +08:00
DLmaster
9394c7a9c5 Merge branch 'dev' 2025-04-13 16:52:12 +08:00
DLmaster
7e502420fa fix: 修复更新器无法下载MAA的异常 2025-04-13 16:46:54 +08:00
DLmaster
12f4b764de Merge branch 'dev' 2025-04-13 02:50:34 +08:00
DLmaster
4da4b7d552 fix: 发布到stable 2025-04-13 02:50:23 +08:00
DLmaster
d38abbbaa0 Merge branch 'dev' 2025-04-13 00:46:14 +08:00
DLmaster
67bf7f649e fix(utils): 改回单文件打包 2025-04-13 00:45:47 +08:00
DLmaster
acb35403b0 Merge branch 'main' into dev 2025-04-12 23:10:19 +08:00
DLmaster
7d5dccc649 fix(ui): 修复更新器无法启动的异常 2025-04-12 23:09:47 +08:00
DLmaster
a7e0e7b217 Merge branch 'dev' 2025-04-12 21:11:25 +08:00
DLmaster
9ce75b2dda fix(ci): 规避v4.3.0错误包 2025-04-12 21:11:05 +08:00
DLmaster
d2022819f6 Merge branch 'dev' 2025-04-12 19:03:54 +08:00
DLmaster
c8b342ba01 fix(core): 修复版本号问题 2025-04-12 19:03:39 +08:00
DLmaster
63823d5c89 Merge branch 'dev' 2025-04-12 16:54:03 +08:00
DLmaster
cb17cc32da feat(ci): 发布v4.3.0.0 2025-04-12 16:53:40 +08:00
DLmaster
14d0e6d438 fix(ci): 打包流程修复 2025-04-12 12:08:13 +08:00
DLmaster
878fbad06a fix(ci): 打包流程更正 2025-04-12 10:31:00 +08:00
DLmaster
deb0506163 feat(ui): 添加MirrorChyan购买链接 2025-04-12 09:46:29 +08:00
DLmaster
c4aeb673fd fix(ci): 恢复单文件打包方法 2025-04-12 09:38:34 +08:00
DLmaster
915ee59643 fix(core): 清理临时更新器改为主程序退出时进行 2025-04-12 06:43:43 +08:00
DLmaster
1568e120be Merge branch 'user_dashboard_dev' into dev 2025-04-12 06:18:39 +08:00
DLmaster
d19dd3496d feat(gui): 优化与修复
- 添加用户仪表盘子界面
- 更新逻辑修复
- 获取关卡号,用户密码解密逻辑优化
2025-04-12 06:18:20 +08:00
DLmaster
62c86ce477 feat(ci): 重新激活流程 2025-04-11 21:06:35 +08:00
DLmaster
c727eddc54 fix(ci): 添加写权限 2025-04-11 20:56:40 +08:00
DLmaster
9c946ef6dc feat(ci): 适配最新打包代码 2025-04-11 19:10:59 +08:00
DLmaster
38a04fc4b2 Squashed commit of the following:
commit 8724c545a8af8f34565aa71620e66cbd71547f37
Author: DLmaster <DLmaster_361@163.com>
Date:   Fri Apr 11 18:08:28 2025 +0800

    feat(core): 预接入mirrorc

commit d57ebaa281ff7c418aa8f11fe8e8ba260d8dbeca
Author: DLmaster <DLmaster_361@163.com>
Date:   Thu Apr 10 12:37:26 2025 +0800

    chore(core): 基础配置相关内容重构

    - 添加理智药设置选项 #34
    - 输入对话框添加回车键确认能力 #35
    - 用户列表UI改版升级
    - 配置类取消单例限制
    - 配置读取方式与界面渲染方法优化

commit 710542287d04719c8443b91acb227de1dccc20d0
Author: DLmaster <DLmaster_361@163.com>
Date:   Fri Mar 28 23:32:17 2025 +0800

    chore(core): search相关结构重整

commit 8009c69236655e29119ce62ff53a0360abaed2af
Merge: 648f42b 9f88f92
Author: DLmaster <DLmaster_361@163.com>
Date:   Mon Mar 24 15:31:40 2025 +0800

    Merge branch 'dev' into user_list_dev
2025-04-11 18:57:10 +08:00
bded794647 Merge remote-tracking branch 'upstream/main' into dev
# Conflicts:
#	.github/workflows/build-app.yml
#	.github/workflows/build-pre.yml
#	app/core/config.py
2025-04-11 10:37:21 +08:00
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
d7e502e22f fix(ui): 对win10主题进一步适配 2025-04-09 11:22:50 +08:00
DLmaster
bbeab360bc Merge pull request #36 from Nether-Dream/dev
对主题代码优化,使其尽量适配Win10
2025-04-09 11:08:51 +08:00
Nether-Dream
a78b7fdb29 对主题代码优化,使其尽量适配Win10
因为是Win10环境修改,需要Win11用户进行测试
2025-04-09 10:33:34 +08:00
273fbe2261 fix(MAA): 修复技巧概要·卷1等技能书不能被掉落统计正确统计。 2025-03-26 14:26:06 +08:00
ba9855c616 fix(notify): 修复 ServerChan 消息推送的一个多余的字母 2025-03-25 14:41:18 +08:00
c54f894f4f fix(notify): 修复 ServerChan 消息换行显示异常问题 2025-03-25 14:14:06 +08:00
DLmaster
9f88f92ec0 Merge branch 'notification_dev' into dev 2025-03-24 15:07:07 +08:00
DLmaster
a80e96c2cd feat(notification): 程序优化
- loguru开始捕获子线程异常
- 通知服务添加校验项
2025-03-24 15:05:27 +08:00
DLmaster
7774612810 Merge branch 'notification_dev' into dev 2025-03-24 11:46:46 +08:00
088ea1817c feat(notification): 添加测试通知功能 2025-03-24 10:15:39 +08:00
DLmaster
f362c8f7ef Merge branch 'user_list_dev' into dev 2025-03-21 22:48:57 +08:00
DLmaster
648f42b7e0 fix(core): 修复版本更新相关的若干问题
- 修复更新器解压失败问题
- 主程序版本号完全写死在代码内部
2025-03-21 22:48:05 +08:00
DLmaster
9a56cc350d fix(core): 修复更新通知阻碍调度开始问题 #32 2025-03-20 19:55:15 +08:00
DLmaster
50cd49217f Merge branch 'annihilation_dev' into dev 2025-03-20 12:42:03 +08:00
DLmaster
7ed4b7db57 fix(core): 补充版本信息文件 2025-03-20 12:41:47 +08:00
b359cd623b feat(maa): 添加每周剿灭模式上限功能 2025-03-20 10:42:46 +08:00
DLmaster
a363e8dc34 fix(utils): 修复打包中版本信息生成方法 2025-03-18 23:05:20 +08:00
DLmaster
52affc0d76 feat(utils): 更新器优化
- 更新信息样式优化
- 更新器支持动态获取下载站
2025-03-18 22:56:33 +08: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
dd022cf356 Merge branch 'Notice_dev' into dev 2025-03-16 01:10:25 +08:00
DLmaster
62e5bb30e2 feat(ui): 公告样式优化 2025-03-16 01:10:13 +08:00
DLmaster
675e11960a fix(ui): 补充网址外链调色 2025-03-16 01:01:23 +08:00
DLmaster
0c274ecbe0 feat(ui): 初步完成公告界面升级 2025-03-16 00:22:24 +08:00
DLmaster
2dfcd3f131 fix(core): 日志版本号更新 2025-03-15 17:52:31 +08:00
DLmaster
053acd138f fix(models): 修复MAA超时判定异常失效 2025-03-15 14:02:47 +08:00
DLmaster
3f20ae62be fix(models): 修复检测到MAA未能实际执行任务报错被异常屏蔽 2025-03-15 13:21:52 +08:00
DLmaster
d342c7c827 Merge branch 'DLMS_dev' into dev 2025-03-15 13:11:52 +08:00
DLmaster
3da0cfd0d0 feat(core): 添加强制关闭ADB与模拟器等增强任务项 2025-03-15 13:11:39 +08:00
DLmaster
acc4045580 Merge branch 'DLMS_dev' into dev 2025-03-15 00:00:57 +08:00
DLmaster
6ee577302f fix(core): 人工排查时自动屏蔽静默操作 2025-03-14 23:59:55 +08:00
DLmaster
d52856180a fix(maa): 人工排查弹窗方法优化 2025-03-14 23:35:58 +08:00
DLmaster
d4d479ca20 feat(ui): 关机等电源操作添加100s倒计时 2025-03-14 23:06:51 +08:00
DLmaster
364af4b9c5 fix(ui): 修复密码显示按钮动画异常 2025-03-13 21:15:45 +08:00
DLmaster
9e0d81fb1d Merge branch 'updater_dev' into dev 2025-03-13 21:00:37 +08:00
DLmaster
2ee2c37479 feat(utils): 更新器支持指定线程数 2025-03-13 21:00:09 +08:00
DLmaster
528925b969 feat(utils): 更新器初步支持多线程下载 2025-03-13 20:21:25 +08:00
4851b40777 fix(main_window): 修改网络错误提示,不展示具体报错信息,只在log中展示 2025-03-10 16:51:47 +08:00
DLmaster
6372ad4e0a Merge pull request #31 from Zrief/dev
Update main_info_bar.py
2025-03-09 22:12:03 +08:00
Zrief
465bc9137e Update main_info_bar.py
简化代码长度
2025-03-09 18:51:41 +08:00
DLmaster
e8b6f5d893 feat(core): 屏蔽MuMu模拟器开屏广告功能上线 2025-03-07 17:48:43 +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
483 changed files with 64833 additions and 815 deletions

31
.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
logs/
*.egg-info
# Virtual environments
.venv
.python-version
list/
uv.lock
# IDE and editors
.vscode/
.idea/
*.swp
*.swo
.DS_Store
# User files
config/
data/
debug/
history/
script/
res/notice.json
res/theme_image.json
res/images/Home/BannerTheme.jpg

Binary file not shown.

View File

@@ -1,29 +0,0 @@
import sqlite3
import subprocess
import datetime
import time
import os
from termcolor import colored
DATABASE="data/data.db"
db=sqlite3.connect(DATABASE)
cur=db.cursor()
cur.execute("SELECT * FROM timeset WHERE True")
timeset=cur.fetchall()
timeset=[list(row) for row in timeset]
while True:
curtime=datetime.datetime.now().strftime("%H:%M")
print(colored("当前时间:"+curtime,'green'))
timenew=[]
timenew.append(curtime)
if timenew in timeset:
print(colored("开始执行",'yellow'))
maa=subprocess.Popen(["run.exe"])
maapid=maa.pid
while True:
if os.path.exists("OVER"):
os.system('taskkill /F /T /PID '+str(maapid))
os.remove("OVER")
print(colored("执行完毕",'yellow'))
break
time.sleep(1)

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

254
README.md
View File

@@ -1,252 +1,2 @@
# AUTO_MAA
MAA多账号管理与自动化软件
----------------------------------------------------------------------------------------------
## 免责声明
本软件是一个外部工具旨在优化MAA多账号功能体验。该软件包可以存储多账号数据并通过修改MAA配置文件、读取MAA日志等行为自动完成多账号代理。
This software is open source, free of charge and for learning and exchange purposes only. The developer team has the final right to interpret this project. All problems arising from the use of this software are not related to this project and the developer team. If you encounter a merchant using this software to practice on your behalf and charging for it, it may be the cost of equipment and time, etc. The problems and consequences arising from this software have nothing to do with it.
本软件开源、免费,仅供学习交流使用。开发者团队拥有本项目的最终解释权。使用本软件产生的所有问题与本项目与开发者团队无关。若您遇到商家使用本软件进行代练并收费,可能是设备与时间等费用,产生的问题及后果与本软件无关。
## 安装与配置MAA
本软件是MAA的外部工具需要安装配置MAA后才能使用。
#### MAA安装
什么是MAA [官网](https://maa.plus/)/[GitHub](https://github.com/CHNZYX/Auto_Simulated_Universe/archive/refs/heads/main.zip)
MAA下载地址 [GitHub下载](https://github.com/MaaAssistantArknights/MaaAssistantArknights/releases)
#### MAA配置
1.完成MAA的adb配置等基本配置
2.在“完成后”菜单选择“退出MAA和模拟器”。勾选“手动输入关卡名”和“无限吃48小时内过期的理智药”
![MAA配置1](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/MAA配置1.png "MAA配置1")
3.确保当前配置名为“Default”取消所有“定时执行”
![MAA配置2](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/MAA配置2.png "MAA配置2")
4.取消勾选“开机自启动MAA”勾选“启动MAA后直接运行”和“启动MAA后自动开启模拟器”。配置自己模拟器所在的位置并根据实际情况填写“等待模拟器启动时间”建议预留10s以防意外。如果是多开用户需要填写“附加命令”具体填写值参见多开模拟器对应快捷方式路径如“-v 1”
![MAA配置3](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/MAA配置3.png "MAA配置3")
5.勾选“定时检查更新”、“自动下载更新包”和“自动安装更新包”
![MAA配置4](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/MAA配置4.png "MAA配置4")
## 下载AUTO_MAA软件包 [![](https://img.shields.io/github/downloads/DLmaster361/AUTO_MAA/total?color=66ccff)](https://github.com/DLmaster361/AUTO_MAA/releases)
GitHub下载地址 [GitHub下载](https://github.com/DLmaster361/AUTO_MAA/releases)
## 配置用户信息与相关参数
注意当前所有的密码输入部分都存在一点“小问题”请在输入密码时避免输入Delete、F12、Tab等功能键。
-------------------------------------------------
#### 第一次启动
双击启动`manage.exe`输入MAA所在文件夹路径并回车注意使用斜杠的种类不要使用反斜杠然后设置管理密钥。
![信息配置1](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/信息配置1.png "MAA配置1")
管理密钥是解密用户密码的唯一凭证,与数据库绑定。密钥丢失或`data/key/`目录下任一文件损坏都将导致解密无法正常进行。
本项目采用自主组建的混合加密模式,项目组也无法找回您的管理密钥或修复`data/key/`目录下的文件。如果不幸的事发生,建议您删除`data/data.db`重新录入信息。
当前暂不支持修改管理密钥,请等待后续更新。
#### 添加用户
输入“+”以开始添加用户。依次输入:
用户名:管理用户的惟一凭证
手机号码:允许隐去中间四位以“****”代替
代理天数:这个还要我解释吗?
密码:警告!密码功能暂未开发,输入的信息会以明文存储,有泄露风险,请勿使用。可以用无意义的字符串代替。由于忽略警告导致的信息泄露,本项目组概不负责
![信息配置2](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/信息配置2.png "MAA配置2")
#### 删除用户
输入用户名+“-”以删除用户。格式:
```plaintext
用户名 -
```
![信息配置3](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/信息配置3.png "MAA配置3")
#### 配置用户状态
启用代理:输入用户名+“y”以启用该用户的代理。格式
```plaintext
用户名 y
```
![信息配置4](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/信息配置4.png "MAA配置4")
禁用代理:输入用户名+“n”以禁用该用户的代理。格式
```plaintext
用户名 n
```
![信息配置5](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/信息配置5.png "MAA配置5")
#### 续期
输入用户名+续期天数+“+”以延长该用户的代理天数。格式:
```plaintext
用户名 续期天数 +
```
![信息配置6](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/信息配置6.png "MAA配置6")
#### 修改刷取关卡
输入用户名+关卡号+“~”以更改该用户的代理关卡。格式:
```plaintext
用户名 关卡号 ~
```
![信息配置7](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/信息配置7.png "MAA配置7")
特别的:
你可以自定义关卡号替换方案。程序会读取`gameid.txt`中的数据,依据此进行关卡号的替换,便于常用关卡的使用。`gameid.txt`在初始已经存储了一些常用资源本的替代方案。
![gameid](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/gameid.png "gameid")
#### 设置MAA路径
输入“/”+新的MAA文件夹路径以修改MAA安装位置的配置。格式
```plaintext
/新的MAA文件夹路径
```
注意:‘/’与路径间没有空格,路径同样不能使用反斜杠
![信息配置8](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/信息配置8.png "MAA配置8")
#### 设置启动时间
添加启动时间:输入“:+”+时间以添加定时启动时间。格式:
```plaintext
:+小时:分钟
```
注意:所有输入间没有空格
![信息配置9](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/信息配置9.png "MAA配置9")
删除启动时间:输入“:-”+时间以删除定时启动时间。格式:
```plaintext
:-小时:分钟
```
注意:所有输入间没有空格
![信息配置10](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/信息配置10.png "MAA配置10")
#### 检索信息
检索所有信息:`manage.exe`打开时会打印所有用户与配置信息。除此之外你可以通过输入“all ?”以打印所有信息,如下:
```plaintext
all ?
```
![信息配置11](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/信息配置11.png "MAA配置11")
检索MAA路径输入“maa ?”以检索MAA安装路径如下
```plaintext
maa ?
```
![信息配置12](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/信息配置12.png "MAA配置12")
检索启动时间输入“time ?”以检索定时启动的时间,如下:
```plaintext
time ?
```
![信息配置13](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/信息配置13.png "MAA配置13")
检索指定用户:输入用户名+“?”以检索指定用户信息,如下:
```plaintext
用户名 ?
```
![信息配置14](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/信息配置14.png "MAA配置14")
#### 退出
输入“-”以退出`manage.exe`,如下:
```plaintext
-
```
## 运行代理
#### 直接运行
双击`run.exe`直接运行
#### 定时运行
双击`AUTO_MAA.exe`打开,不要关闭。它会读取设定时间,在该时刻自动运行
注意:周一将自动进行剿灭代理
## 关于
项目图标由文心一格AI生成
----------------------------------------------------------------------------------------------
欢迎加入欢迎反馈bug
QQ群没有
----------------------------------------------------------------------------------------------
如果喜欢本项目,可以打赏送作者一杯咖啡喵!
![打赏](https://github.com/DLmaster361/AUTO_MAA/blob/main/res/README/payid.png "打赏")
----------------------------------------------------------------------------------------------
## 贡献者
感谢以下贡献者对本项目做出的贡献
<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")
## 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)
TEST
TEST

34
app/__init__.py Normal file
View File

@@ -0,0 +1,34 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
__version__ = "5.0.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .api import *
from .core import *
from .models import *
from .services import *
from .utils import *
__all__ = ["api", "core", "models", "services", "utils"]

47
app/api/__init__.py Normal file
View File

@@ -0,0 +1,47 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
__version__ = "5.0.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .core import router as core_router
from .info import router as info_router
from .scripts import router as scripts_router
from .plan import router as plan_router
from .queue import router as queue_router
from .dispatch import router as dispatch_router
from .history import router as history_router
from .setting import router as setting_router
from .update import router as update_router
__all__ = [
"core_router",
"info_router",
"scripts_router",
"plan_router",
"queue_router",
"dispatch_router",
"history_router",
"setting_router",
"update_router",
]

95
app/api/core.py Normal file
View File

@@ -0,0 +1,95 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import time
import asyncio
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from app.core import Config, Broadcast, TaskManager
from app.services import System
from app.models.schema import *
router = APIRouter(prefix="/api/core", tags=["核心信息"])
@router.websocket("/ws")
async def connect_websocket(websocket: WebSocket):
if Config.websocket is not None:
await websocket.close(code=1000, reason="已有连接")
return
await websocket.accept()
Config.websocket = websocket
last_pong = time.monotonic()
last_ping = time.monotonic()
data = {}
await TaskManager.start_startup_queue()
while True:
try:
data = await asyncio.wait_for(websocket.receive_json(), timeout=1000005.0)
if data.get("type") == "Signal" and "Pong" in data.get("data", {}):
last_pong = time.monotonic()
elif data.get("type") == "Signal" and "Ping" in data.get("data", {}):
await websocket.send_json(
WebSocketMessage(
id="Main", type="Signal", data={"Pong": "无描述"}
).model_dump()
)
else:
await Broadcast.put(data)
except asyncio.TimeoutError:
if last_pong < last_ping:
await websocket.close(code=1000, reason="Ping超时")
break
await websocket.send_json(
WebSocketMessage(
id="Main", type="Signal", data={"Ping": "无描述"}
).model_dump()
)
last_ping = time.monotonic()
except WebSocketDisconnect:
break
Config.websocket = None
await System.set_power("KillSelf")
@router.post("/close")
async def close():
"""关闭后端程序"""
try:
await System.set_power("KillSelf")
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()

87
app/api/dispatch.py Normal file
View File

@@ -0,0 +1,87 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
from fastapi import APIRouter, Body
from app.core import Config, TaskManager
from app.services import System
from app.models.schema import *
router = APIRouter(prefix="/api/dispatch", tags=["任务调度"])
@router.post(
"/start", summary="添加任务", response_model=TaskCreateOut, status_code=200
)
async def add_task(task: TaskCreateIn = Body(...)) -> TaskCreateOut:
try:
task_id = await TaskManager.add_task(task.mode, task.taskId)
except Exception as e:
return TaskCreateOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
websocketId="",
)
return TaskCreateOut(websocketId=str(task_id))
@router.post("/stop", summary="中止任务", response_model=OutBase, status_code=200)
async def stop_task(task: DispatchIn = Body(...)) -> OutBase:
try:
await TaskManager.stop_task(task.taskId)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/set/power", summary="设置电源标志", response_model=OutBase, status_code=200
)
async def set_power(task: PowerIn = Body(...)) -> OutBase:
try:
Config.power_sign = task.signal
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/cancel/power", summary="取消电源任务", response_model=OutBase, status_code=200
)
async def cancel_power_task() -> OutBase:
try:
await System.cancel_power_task()
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()

88
app/api/history.py Normal file
View File

@@ -0,0 +1,88 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
from datetime import datetime
from pathlib import Path
from fastapi import APIRouter, Body
from app.core import Config
from app.models.schema import *
router = APIRouter(prefix="/api/history", tags=["历史记录"])
@router.post(
"/search",
summary="搜索历史记录总览信息",
response_model=HistorySearchOut,
status_code=200,
)
async def search_history(history: HistorySearchIn) -> HistorySearchOut:
try:
data = await Config.search_history(
history.mode,
datetime.strptime(history.start_date, "%Y-%m-%d").date(),
datetime.strptime(history.end_date, "%Y-%m-%d").date(),
)
for date, users in data.items():
for user, records in users.items():
record = await Config.merge_statistic_info(records)
# 安全检查:确保 index 字段存在
if "index" not in record:
record["index"] = []
record["index"] = [HistoryIndexItem(**_) for _ in record["index"]]
record = HistoryData(**record)
data[date][user] = record
except Exception as e:
return HistorySearchOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
data={},
)
return HistorySearchOut(data=data)
@router.post(
"/data",
summary="从指定文件内获取历史记录数据",
response_model=HistoryDataGetOut,
status_code=200,
)
async def get_history_data(history: HistoryDataGetIn = Body(...)) -> HistoryDataGetOut:
try:
path = Path(history.jsonPath)
data = await Config.merge_statistic_info([path])
data.pop("index", None)
data["log_content"] = path.with_suffix(".log").read_text(encoding="utf-8")
data = HistoryData(**data)
except Exception as e:
return HistoryDataGetOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
data=HistoryData(**{}),
)
return HistoryDataGetOut(data=data)

215
app/api/info.py Normal file
View File

@@ -0,0 +1,215 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
from fastapi import APIRouter, Body
from app.core import Config
from app.models.schema import *
router = APIRouter(prefix="/api/info", tags=["信息获取"])
@router.post(
"/version",
summary="获取后端git版本信息",
response_model=VersionOut,
status_code=200,
)
async def get_git_version() -> VersionOut:
try:
is_latest, commit_hash, commit_time = await Config.get_git_version()
except Exception as e:
return VersionOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
if_need_update=False,
current_hash="unknown",
current_time="unknown",
current_version=Config.version(),
)
return VersionOut(
if_need_update=not is_latest,
current_hash=commit_hash,
current_time=commit_time,
current_version=Config.version(),
)
@router.post(
"/combox/stage",
summary="获取关卡号下拉框信息",
response_model=ComboBoxOut,
status_code=200,
)
async def get_stage_combox(
stage: GetStageIn = Body(..., description="关卡号类型")
) -> ComboBoxOut:
try:
raw_data = await Config.get_stage_info(stage.type)
data = (
[ComboBoxItem(**item) for item in raw_data if isinstance(item, dict)]
if raw_data
else []
)
except Exception as e:
return ComboBoxOut(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[]
)
return ComboBoxOut(data=data)
@router.post(
"/combox/script",
summary="获取脚本下拉框信息",
response_model=ComboBoxOut,
status_code=200,
)
async def get_script_combox() -> ComboBoxOut:
try:
raw_data = await Config.get_script_combox()
data = [ComboBoxItem(**item) for item in raw_data] if raw_data else []
except Exception as e:
return ComboBoxOut(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[]
)
return ComboBoxOut(data=data)
@router.post(
"/combox/task",
summary="获取可选任务下拉框信息",
response_model=ComboBoxOut,
status_code=200,
)
async def get_task_combox() -> ComboBoxOut:
try:
raw_data = await Config.get_task_combox()
data = [ComboBoxItem(**item) for item in raw_data] if raw_data else []
except Exception as e:
return ComboBoxOut(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[]
)
return ComboBoxOut(data=data)
@router.post(
"/combox/plan",
summary="获取可选计划下拉框信息",
response_model=ComboBoxOut,
status_code=200,
)
async def get_plan_combox() -> ComboBoxOut:
try:
raw_data = await Config.get_plan_combox()
data = [ComboBoxItem(**item) for item in raw_data] if raw_data else []
except Exception as e:
return ComboBoxOut(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[]
)
return ComboBoxOut(data=data)
@router.post(
"/notice/get", summary="获取通知信息", response_model=NoticeOut, status_code=200
)
async def get_notice_info() -> NoticeOut:
try:
if_need_show, data = await Config.get_notice()
except Exception as e:
return NoticeOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
if_need_show=False,
data={},
)
return NoticeOut(if_need_show=if_need_show, data=data)
@router.post(
"/notice/confirm", summary="确认通知", response_model=OutBase, status_code=200
)
async def confirm_notice() -> OutBase:
try:
await Config.set("Data", "IfShowNotice", False)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
# @router.post(
# "/apps_info", summary="获取可下载应用信息", response_model=InfoOut, status_code=200
# )
# async def get_apps_info() -> InfoOut:
# try:
# data = await Config.get_server_info("apps_info")
# except Exception as e:
# return InfoOut(
# code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data={}
# )
# return InfoOut(data=data)
@router.post(
"/webconfig",
summary="获取配置分享中心的配置信息",
response_model=InfoOut,
status_code=200,
)
async def get_web_config() -> InfoOut:
try:
data = await Config.get_web_config()
except Exception as e:
return InfoOut(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data={}
)
return InfoOut(data={"WebConfig": data})
@router.post(
"/get/overview", summary="信息总览", response_model=InfoOut, status_code=200
)
async def get_overview() -> InfoOut:
try:
stage = await Config.get_stage_info("Info")
proxy = await Config.get_proxy_overview()
except Exception as e:
return InfoOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
data={"Stage": [], "Proxy": []},
)
return InfoOut(data={"Stage": stage, "Proxy": proxy})

106
app/api/plan.py Normal file
View File

@@ -0,0 +1,106 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
from fastapi import APIRouter, Body
from app.core import Config
from app.models.schema import *
router = APIRouter(prefix="/api/plan", tags=["计划管理"])
@router.post(
"/add", summary="添加计划表", response_model=PlanCreateOut, status_code=200
)
async def add_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut:
try:
uid, config = await Config.add_plan(plan.type)
data = MaaPlanConfig(**(await config.toDict()))
except Exception as e:
return PlanCreateOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
planId="",
data=MaaPlanConfig(**{}),
)
return PlanCreateOut(planId=str(uid), data=data)
@router.post("/get", summary="查询计划表", response_model=PlanGetOut, status_code=200)
async def get_plan(plan: PlanGetIn = Body(...)) -> PlanGetOut:
try:
index, data = await Config.get_plan(plan.planId)
index = [PlanIndexItem(**_) for _ in index]
data = {uid: MaaPlanConfig(**cfg) for uid, cfg in data.items()}
except Exception as e:
return PlanGetOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
index=[],
data={},
)
return PlanGetOut(index=index, data=data)
@router.post(
"/update", summary="更新计划表配置信息", response_model=OutBase, status_code=200
)
async def update_plan(plan: PlanUpdateIn = Body(...)) -> OutBase:
try:
await Config.update_plan(plan.planId, plan.data.model_dump(exclude_unset=True))
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post("/delete", summary="删除计划表", response_model=OutBase, status_code=200)
async def delete_plan(plan: PlanDeleteIn = Body(...)) -> OutBase:
try:
await Config.del_plan(plan.planId)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/order", summary="重新排序计划表", response_model=OutBase, status_code=200
)
async def reorder_plan(plan: PlanReorderIn = Body(...)) -> OutBase:
try:
await Config.reorder_plan(plan.indexList)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()

259
app/api/queue.py Normal file
View File

@@ -0,0 +1,259 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
from fastapi import APIRouter, Body
from app.core import Config
from app.models.schema import *
router = APIRouter(prefix="/api/queue", tags=["调度队列管理"])
@router.post(
"/add", summary="添加调度队列", response_model=QueueCreateOut, status_code=200
)
async def add_queue() -> QueueCreateOut:
try:
uid, config = await Config.add_queue()
data = QueueConfig(**(await config.toDict()))
except Exception as e:
return QueueCreateOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
queueId="",
data=QueueConfig(**{}),
)
return QueueCreateOut(queueId=str(uid), data=data)
@router.post(
"/get", summary="查询调度队列配置信息", response_model=QueueGetOut, status_code=200
)
async def get_queues(queue: QueueGetIn = Body(...)) -> QueueGetOut:
try:
index, config = await Config.get_queue(queue.queueId)
index = [QueueIndexItem(**_) for _ in index]
data = {uid: QueueConfig(**cfg) for uid, cfg in config.items()}
except Exception as e:
return QueueGetOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
index=[],
data={},
)
return QueueGetOut(index=index, data=data)
@router.post(
"/update", summary="更新调度队列配置信息", response_model=OutBase, status_code=200
)
async def update_queue(queue: QueueUpdateIn = Body(...)) -> OutBase:
try:
await Config.update_queue(
queue.queueId, queue.data.model_dump(exclude_unset=True)
)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post("/delete", summary="删除调度队列", response_model=OutBase, status_code=200)
async def delete_queue(queue: QueueDeleteIn = Body(...)) -> OutBase:
try:
await Config.del_queue(queue.queueId)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post("/order", summary="重新排序", response_model=OutBase, status_code=200)
async def reorder_queue(script: QueueReorderIn = Body(...)) -> OutBase:
try:
await Config.reorder_queue(script.indexList)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/time/get", summary="查询定时项", response_model=TimeSetGetOut, status_code=200
)
async def get_time_set(time: TimeSetGetIn = Body(...)) -> TimeSetGetOut:
try:
index, data = await Config.get_time_set(time.queueId, time.timeSetId)
index = [TimeSetIndexItem(**_) for _ in index]
data = {uid: TimeSet(**cfg) for uid, cfg in data.items()}
except Exception as e:
return TimeSetGetOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
index=[],
data={},
)
return TimeSetGetOut(index=index, data=data)
@router.post(
"/time/add", summary="添加定时项", response_model=TimeSetCreateOut, status_code=200
)
async def add_time_set(time: QueueSetInBase = Body(...)) -> TimeSetCreateOut:
uid, config = await Config.add_time_set(time.queueId)
data = TimeSet(**(await config.toDict()))
return TimeSetCreateOut(timeSetId=str(uid), data=data)
@router.post(
"/time/update", summary="更新定时项", response_model=OutBase, status_code=200
)
async def update_time_set(time: TimeSetUpdateIn = Body(...)) -> OutBase:
try:
await Config.update_time_set(
time.queueId, time.timeSetId, time.data.model_dump(exclude_unset=True)
)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/time/delete", summary="删除定时项", response_model=OutBase, status_code=200
)
async def delete_time_set(time: TimeSetDeleteIn = Body(...)) -> OutBase:
try:
await Config.del_time_set(time.queueId, time.timeSetId)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/time/order", summary="重新排序定时项", response_model=OutBase, status_code=200
)
async def reorder_time_set(time: TimeSetReorderIn = Body(...)) -> OutBase:
try:
await Config.reorder_time_set(time.queueId, time.indexList)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/item/get", summary="查询队列项", response_model=QueueItemGetOut, status_code=200
)
async def get_item(item: QueueItemGetIn = Body(...)) -> QueueItemGetOut:
try:
index, data = await Config.get_queue_item(item.queueId, item.queueItemId)
index = [QueueItemIndexItem(**_) for _ in index]
data = {uid: QueueItem(**cfg) for uid, cfg in data.items()}
except Exception as e:
return QueueItemGetOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
index=[],
data={},
)
return QueueItemGetOut(index=index, data=data)
@router.post(
"/item/add",
summary="添加队列项",
response_model=QueueItemCreateOut,
status_code=200,
)
async def add_item(item: QueueSetInBase = Body(...)) -> QueueItemCreateOut:
uid, config = await Config.add_queue_item(item.queueId)
data = QueueItem(**(await config.toDict()))
return QueueItemCreateOut(queueItemId=str(uid), data=data)
@router.post(
"/item/update", summary="更新队列项", response_model=OutBase, status_code=200
)
async def update_item(item: QueueItemUpdateIn = Body(...)) -> OutBase:
try:
await Config.update_queue_item(
item.queueId, item.queueItemId, item.data.model_dump(exclude_unset=True)
)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/item/delete", summary="删除队列项", response_model=OutBase, status_code=200
)
async def delete_item(item: QueueItemDeleteIn = Body(...)) -> OutBase:
try:
await Config.del_queue_item(item.queueId, item.queueItemId)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/item/order", summary="重新排序队列项", response_model=OutBase, status_code=200
)
async def reorder_item(item: QueueItemReorderIn = Body(...)) -> OutBase:
try:
await Config.reorder_queue_item(item.queueId, item.indexList)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()

283
app/api/scripts.py Normal file
View File

@@ -0,0 +1,283 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import uuid
from fastapi import APIRouter, Body
from app.core import Config
from app.models.schema import *
router = APIRouter(prefix="/api/scripts", tags=["脚本管理"])
SCRIPT_BOOK = {"MaaConfig": MaaConfig, "GeneralConfig": GeneralConfig}
USER_BOOK = {"MaaConfig": MaaUserConfig, "GeneralConfig": GeneralUserConfig}
@router.post(
"/add", summary="添加脚本", response_model=ScriptCreateOut, status_code=200
)
async def add_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut:
try:
uid, config = await Config.add_script(script.type)
data = SCRIPT_BOOK[type(config).__name__](**(await config.toDict()))
except Exception as e:
return ScriptCreateOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
scriptId="",
data=GeneralConfig(**{}),
)
return ScriptCreateOut(scriptId=str(uid), data=data)
@router.post(
"/get", summary="查询脚本配置信息", response_model=ScriptGetOut, status_code=200
)
async def get_script(script: ScriptGetIn = Body(...)) -> ScriptGetOut:
try:
index, data = await Config.get_script(script.scriptId)
index = [ScriptIndexItem(**_) for _ in index]
data = {
uid: SCRIPT_BOOK[next((_.type for _ in index if _.uid == uid), "General")](
**cfg
)
for uid, cfg in data.items()
}
except Exception as e:
return ScriptGetOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
index=[],
data={},
)
return ScriptGetOut(index=index, data=data)
@router.post(
"/update", summary="更新脚本配置信息", response_model=OutBase, status_code=200
)
async def update_script(script: ScriptUpdateIn = Body(...)) -> OutBase:
try:
await Config.update_script(
script.scriptId, script.data.model_dump(exclude_unset=True)
)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post("/delete", summary="删除脚本", response_model=OutBase, status_code=200)
async def delete_script(script: ScriptDeleteIn = Body(...)) -> OutBase:
try:
await Config.del_script(script.scriptId)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post("/order", summary="重新排序脚本", response_model=OutBase, status_code=200)
async def reorder_script(script: ScriptReorderIn = Body(...)) -> OutBase:
try:
await Config.reorder_script(script.indexList)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/import/file", summary="从文件加载脚本", response_model=OutBase, status_code=200
)
async def import_script_from_file(script: ScriptFileIn = Body(...)) -> OutBase:
try:
await Config.import_script_from_file(script.scriptId, script.jsonFile)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/export/file", summary="导出脚本到文件", response_model=OutBase, status_code=200
)
async def export_script_to_file(script: ScriptFileIn = Body(...)) -> OutBase:
try:
await Config.export_script_to_file(script.scriptId, script.jsonFile)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/import/web", summary="从网络加载脚本", response_model=OutBase, status_code=200
)
async def import_script_from_web(script: ScriptUrlIn = Body(...)) -> OutBase:
try:
await Config.import_script_from_web(script.scriptId, script.url)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/Upload/web", summary="上传脚本配置到网络", response_model=OutBase, status_code=200
)
async def upload_script_to_web(script: ScriptUploadIn = Body(...)) -> OutBase:
try:
await Config.upload_script_to_web(
script.scriptId, script.config_name, script.author, script.description
)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/user/get", summary="查询用户", response_model=UserGetOut, status_code=200
)
async def get_user(user: UserGetIn = Body(...)) -> UserGetOut:
try:
index, data = await Config.get_user(user.scriptId, user.userId)
index = [UserIndexItem(**_) for _ in index]
data = {
uid: USER_BOOK[
type(Config.ScriptConfig[uuid.UUID(user.scriptId)]).__name__
](**cfg)
for uid, cfg in data.items()
}
except Exception as e:
return UserGetOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
index=[],
data={},
)
return UserGetOut(index=index, data=data)
@router.post(
"/user/add", summary="添加用户", response_model=UserCreateOut, status_code=200
)
async def add_user(user: UserInBase = Body(...)) -> UserCreateOut:
try:
uid, config = await Config.add_user(user.scriptId)
data = USER_BOOK[type(Config.ScriptConfig[uuid.UUID(user.scriptId)]).__name__](
**(await config.toDict())
)
except Exception as e:
return UserCreateOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
userId="",
data=GeneralUserConfig(**{}),
)
return UserCreateOut(userId=str(uid), data=data)
@router.post(
"/user/update", summary="更新用户配置信息", response_model=OutBase, status_code=200
)
async def update_user(user: UserUpdateIn = Body(...)) -> OutBase:
try:
await Config.update_user(
user.scriptId, user.userId, user.data.model_dump(exclude_unset=True)
)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/user/delete", summary="删除用户", response_model=OutBase, status_code=200
)
async def delete_user(user: UserDeleteIn = Body(...)) -> OutBase:
try:
await Config.del_user(user.scriptId, user.userId)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/user/order", summary="重新排序用户", response_model=OutBase, status_code=200
)
async def reorder_user(user: UserReorderIn = Body(...)) -> OutBase:
try:
await Config.reorder_user(user.scriptId, user.indexList)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/user/infrastructure",
summary="导入基建配置文件",
response_model=OutBase,
status_code=200,
)
async def import_infrastructure(user: UserSetIn = Body(...)) -> OutBase:
try:
await Config.set_infrastructure(user.scriptId, user.userId, user.jsonFile)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()

198
app/api/setting.py Normal file
View File

@@ -0,0 +1,198 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import os
from pathlib import Path
import shutil
from fastapi import APIRouter, Body
from app.core import Config
from app.services import System, Notify
from app.models.schema import *
from app.models.config import Webhook as WebhookConfig
router = APIRouter(prefix="/api/setting", tags=["全局设置"])
@router.post("/get", summary="查询配置", response_model=SettingGetOut, status_code=200)
async def get_scripts() -> SettingGetOut:
"""查询配置"""
try:
data = await Config.get_setting()
except Exception as e:
return SettingGetOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
data=GlobalConfig(**{}),
)
return SettingGetOut(data=GlobalConfig(**data))
@router.post("/update", summary="更新配置", response_model=OutBase, status_code=200)
async def update_script(script: SettingUpdateIn = Body(...)) -> OutBase:
"""更新配置"""
try:
data = script.data.model_dump(exclude_unset=True)
await Config.update_setting(data)
if data.get("Start", {}).get("IfSelfStart", None) is not None:
await System.set_SelfStart()
if data.get("Function", None) is not None:
function = data["Function"]
if function.get("IfAllowSleep", None) is not None:
await System.set_Sleep()
if function.get("IfSkipMumuSplashAds", None) is not None:
MuMu_splash_ads_path = (
Path(os.getenv("APPDATA") or "")
/ "Netease/MuMuPlayer-12.0/data/startupImage"
)
if Config.get("Function", "IfSkipMumuSplashAds"):
if MuMu_splash_ads_path.exists() and MuMu_splash_ads_path.is_dir():
shutil.rmtree(MuMu_splash_ads_path)
MuMu_splash_ads_path.touch()
else:
if MuMu_splash_ads_path.exists() and MuMu_splash_ads_path.is_file():
MuMu_splash_ads_path.unlink()
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/test_notify", summary="测试通知", response_model=OutBase, status_code=200
)
async def test_notify() -> OutBase:
"""测试通知"""
try:
await Notify.send_test_notification()
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/webhook/get",
summary="查询 webhook 配置",
response_model=WebhookGetOut,
status_code=200,
)
async def get_webhook(webhook: WebhookGetIn = Body(...)) -> WebhookGetOut:
try:
index, data = await Config.get_webhook(None, None, webhook.webhookId)
index = [WebhookIndexItem(**_) for _ in index]
data = {uid: Webhook(**cfg) for uid, cfg in data.items()}
except Exception as e:
return WebhookGetOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
index=[],
data={},
)
return WebhookGetOut(index=index, data=data)
@router.post(
"/webhook/add",
summary="添加定时项",
response_model=WebhookCreateOut,
status_code=200,
)
async def add_webhook() -> WebhookCreateOut:
uid, config = await Config.add_webhook(None, None)
data = Webhook(**(await config.toDict()))
return WebhookCreateOut(webhookId=str(uid), data=data)
@router.post(
"/webhook/update", summary="更新定时项", response_model=OutBase, status_code=200
)
async def update_webhook(webhook: WebhookUpdateIn = Body(...)) -> OutBase:
try:
await Config.update_webhook(
None, None, webhook.webhookId, webhook.data.model_dump(exclude_unset=True)
)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/webhook/delete", summary="删除定时项", response_model=OutBase, status_code=200
)
async def delete_webhook(webhook: WebhookDeleteIn = Body(...)) -> OutBase:
try:
await Config.del_webhook(None, None, webhook.webhookId)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/webhook/order", summary="重新排序定时项", response_model=OutBase, status_code=200
)
async def reorder_webhook(webhook: WebhookReorderIn = Body(...)) -> OutBase:
try:
await Config.reorder_webhook(None, None, webhook.indexList)
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post(
"/webhook/test", summary="测试Webhook配置", response_model=OutBase, status_code=200
)
async def test_webhook(webhook: WebhookTestIn = Body(...)) -> OutBase:
"""测试自定义Webhook"""
try:
webhook_config = WebhookConfig()
await webhook_config.load(webhook.data.model_dump())
await Notify.WebhookPush(
"AUTO-MAS Webhook测试",
"这是一条测试消息如果您收到此消息说明Webhook配置正确",
webhook_config,
)
except Exception as e:
return OutBase(code=500, status="error", message=f"Webhook测试失败: {str(e)}")
return OutBase()

82
app/api/update.py Normal file
View File

@@ -0,0 +1,82 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import asyncio
from fastapi import APIRouter, Body
from app.core import Config
from app.services import Updater
from app.models.schema import *
router = APIRouter(prefix="/api/update", tags=["软件更新"])
@router.post(
"/check", summary="检查更新", response_model=UpdateCheckOut, status_code=200
)
async def check_update(version: UpdateCheckIn = Body(...)) -> UpdateCheckOut:
try:
if_need, latest_version, update_info = await Updater.check_update(
current_version=version.current_version, if_force=version.if_force
)
except Exception as e:
return UpdateCheckOut(
code=500,
status="error",
message=f"{type(e).__name__}: {str(e)}",
if_need_update=False,
latest_version="",
update_info={},
)
return UpdateCheckOut(
if_need_update=if_need, latest_version=latest_version, update_info=update_info
)
@router.post("/download", summary="下载更新", response_model=OutBase, status_code=200)
async def download_update() -> OutBase:
try:
task = asyncio.create_task(Updater.download_update())
Config.temp_task.append(task)
task.add_done_callback(lambda t: Config.temp_task.remove(t))
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()
@router.post("/install", summary="安装更新", response_model=OutBase, status_code=200)
async def install_update() -> OutBase:
try:
task = asyncio.create_task(Updater.install_update())
Config.temp_task.append(task)
task.add_done_callback(lambda t: Config.temp_task.remove(t))
except Exception as e:
return OutBase(
code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
)
return OutBase()

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

@@ -0,0 +1,41 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
__version__ = "5.0.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .broadcast import Broadcast
from .config import Config, MaaConfig, GeneralConfig, MaaUserConfig, GeneralUserConfig
from .timer import MainTimer
from .task_manager import TaskManager
__all__ = [
"Broadcast",
"Config",
"MaaConfig",
"GeneralConfig",
"MainTimer",
"TaskManager",
"MaaUserConfig",
"GeneralUserConfig",
]

52
app/core/broadcast.py Normal file
View File

@@ -0,0 +1,52 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import asyncio
from copy import deepcopy
from typing import Set
from app.utils import get_logger
logger = get_logger("消息广播")
class _Broadcast:
def __init__(self):
self.__subscribers: Set[asyncio.Queue] = set()
async def subscribe(self, queue: asyncio.Queue):
"""订阅者注册"""
self.__subscribers.add(queue)
async def unsubscribe(self, queue: asyncio.Queue):
"""取消订阅"""
self.__subscribers.remove(queue)
async def put(self, item):
"""向所有订阅者广播消息"""
for subscriber in self.__subscribers:
await subscriber.put(deepcopy(item))
Broadcast = _Broadcast()

1951
app/core/config.py Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,384 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import uuid
import asyncio
from functools import partial
from typing import Dict, Optional, Literal
from .config import Config, MaaConfig, GeneralConfig, QueueConfig
from app.services import System
from app.models.schema import WebSocketMessage
from app.utils import get_logger
from app.task import *
from app.utils.constants import POWER_SIGN_MAP
logger = get_logger("业务调度")
class _TaskManager:
"""业务调度器"""
def __init__(self):
super().__init__()
self.task_dict: Dict[uuid.UUID, asyncio.Task] = {}
async def add_task(
self, mode: Literal["自动代理", "人工排查", "设置脚本"], uid: str
) -> uuid.UUID:
"""
添加任务
:param mode: 任务模式
:param uid: 任务UID
"""
actual_id = uuid.UUID(uid)
if mode == "设置脚本":
if actual_id in Config.ScriptConfig:
task_id = actual_id
actual_id = None
else:
for script_id, script in Config.ScriptConfig.items():
if actual_id in script.UserData:
task_id = script_id
break
else:
raise ValueError(f"任务 {uid} 无法找到对应脚本配置")
elif actual_id in Config.QueueConfig:
task_id = actual_id
actual_id = None
elif actual_id in Config.ScriptConfig:
task_id = uuid.uuid4()
else:
raise ValueError(f"任务 {uid} 无法找到对应脚本配置")
if task_id in self.task_dict or (
actual_id is not None and actual_id in self.task_dict
):
raise RuntimeError(f"任务 {task_id} 已在运行")
logger.info(f"创建任务: {task_id}, 模式: {mode}")
self.task_dict[task_id] = asyncio.create_task(
self.run_task(mode, task_id, actual_id)
)
self.task_dict[task_id].add_done_callback(
lambda t: asyncio.create_task(self.remove_task(t, mode, task_id))
)
return task_id
@logger.catch
async def run_task(
self, mode: str, task_id: uuid.UUID, actual_id: Optional[uuid.UUID]
):
logger.info(f"开始运行任务: {task_id}, 模式: {mode}")
if mode == "设置脚本":
if isinstance(Config.ScriptConfig[task_id], MaaConfig):
task_item = MaaManager(mode, task_id, actual_id, str(task_id))
elif isinstance(Config.ScriptConfig[task_id], GeneralConfig):
task_item = GeneralManager(mode, task_id, actual_id, str(task_id))
else:
logger.error(
f"不支持的脚本类型: {type(Config.ScriptConfig[task_id]).__name__}"
)
await Config.send_json(
WebSocketMessage(
id=str(task_id),
type="Info",
data={"Error": "脚本类型不支持"},
).model_dump()
)
return
uid = actual_id or uuid.uuid4()
self.task_dict[uid] = asyncio.create_task(task_item.run())
self.task_dict[uid].add_done_callback(
lambda t: asyncio.create_task(task_item.final_task(t))
)
self.task_dict[uid].add_done_callback(partial(self.task_dict.pop, uid))
try:
await self.task_dict[uid]
except Exception as e:
logger.error(f"任务 {task_id} 运行出错: {type(e).__name__}: {str(e)}")
await Config.send_json(
WebSocketMessage(
id=str(task_id),
type="Info",
data={"Error": f"任务运行时出错 {type(e).__name__}: {str(e)}"},
).model_dump()
)
else:
# 初始化任务列表
if task_id in Config.QueueConfig:
task_list = []
for queue_item in Config.QueueConfig[task_id].QueueItem.values():
if queue_item.get("Info", "ScriptId") == "-":
continue
script_uid = uuid.UUID(queue_item.get("Info", "ScriptId"))
task_list.append(
{
"script_id": str(script_uid),
"status": "等待",
"name": Config.ScriptConfig[script_uid].get("Info", "Name"),
"user_list": [
{
"user_id": str(user_id),
"status": "等待",
"name": config.get("Info", "Name"),
}
for user_id, config in Config.ScriptConfig[
script_uid
].UserData.items()
if config.get("Info", "Status")
and config.get("Info", "RemainedDay") != 0
],
}
)
elif actual_id is not None and actual_id in Config.ScriptConfig:
task_list = [
{
"script_id": str(actual_id),
"status": "等待",
"name": Config.ScriptConfig[actual_id].get("Info", "Name"),
"user_list": [
{
"user_id": str(user_id),
"status": "等待",
"name": config.get("Info", "Name"),
}
for user_id, config in Config.ScriptConfig[
actual_id
].UserData.items()
if config.get("Info", "Status")
and config.get("Info", "RemainedDay") != 0
],
}
]
await Config.send_json(
WebSocketMessage(
id=str(task_id), type="Update", data={"task_dict": task_list}
).model_dump()
)
# 清理用户列表初值
for task in task_list:
task.pop("user_list", None)
for task in task_list:
script_id = uuid.UUID(task["script_id"])
# 检查任务是否在运行列表中
if script_id in self.task_dict:
task["status"] = "跳过"
await Config.send_json(
WebSocketMessage(
id=str(task_id),
type="Update",
data={"task_list": task_list},
).model_dump()
)
logger.info(f"跳过任务: {script_id}, 该任务已在运行列表中")
continue
# 检查任务对应脚本是否仍存在
if script_id in self.task_dict:
task["status"] = "异常"
await Config.send_json(
WebSocketMessage(
id=str(task_id),
type="Update",
data={"task_list": task_list},
).model_dump()
)
logger.info(f"跳过任务: {script_id}, 该任务对应脚本已被删除")
continue
# 标记为运行中
task["status"] = "运行"
await Config.send_json(
WebSocketMessage(
id=str(task_id), type="Update", data={"task_list": task_list}
).model_dump()
)
logger.info(f"任务开始: {script_id}")
if isinstance(Config.ScriptConfig[script_id], MaaConfig):
task_item = MaaManager(mode, script_id, None, str(task_id))
elif isinstance(Config.ScriptConfig[script_id], GeneralConfig):
task_item = GeneralManager(mode, script_id, actual_id, str(task_id))
else:
logger.error(
f"不支持的脚本类型: {type(Config.ScriptConfig[script_id]).__name__}"
)
await Config.send_json(
WebSocketMessage(
id=str(task_id),
type="Info",
data={"Error": "脚本类型不支持"},
).model_dump()
)
continue
self.task_dict[script_id] = asyncio.create_task(task_item.run())
self.task_dict[script_id].add_done_callback(
lambda t: asyncio.create_task(task_item.final_task(t))
)
self.task_dict[script_id].add_done_callback(
partial(self.task_dict.pop, script_id)
)
try:
await self.task_dict[script_id]
task["status"] = "完成"
except Exception as e:
logger.error(
f"任务 {script_id} 运行出错: {type(e).__name__}: {str(e)}"
)
await Config.send_json(
WebSocketMessage(
id=str(task_id),
type="Info",
data={
"Error": f"任务运行时出错 {type(e).__name__}: {str(e)}"
},
).model_dump()
)
task["status"] = "异常"
await Config.send_json(
WebSocketMessage(
id=str(task_id),
type="Update",
data={"task_list": task_list},
).model_dump()
)
async def stop_task(self, task_id: str) -> None:
"""
中止任务
:param task_id: 任务ID
"""
logger.info(f"中止任务: {task_id}")
if task_id == "ALL":
for task in self.task_dict.values():
task.cancel()
else:
uid = uuid.UUID(task_id)
if uid not in self.task_dict:
raise ValueError("任务未在运行")
self.task_dict[uid].cancel()
async def remove_task(
self, task: asyncio.Task, mode: str, task_id: uuid.UUID
) -> None:
"""
处理任务结束后的收尾工作
Parameters
----------
task : asyncio.Task
任务对象
mode : str
任务模式
task_id : uuid.UUID
任务ID
"""
logger.info(f"任务结束: {task_id}")
# 从任务字典中移除任务
try:
await task
except asyncio.CancelledError:
logger.info(f"任务 {task_id} 已结束")
self.task_dict.pop(task_id)
await Config.send_json(
WebSocketMessage(
id=str(task_id), type="Signal", data={"Accomplish": "无描述"}
).model_dump()
)
if mode == "自动代理" and task_id in Config.QueueConfig:
if Config.power_sign == "NoAction":
Config.power_sign = Config.QueueConfig[task_id].get(
"Info", "AfterAccomplish"
)
await Config.send_json(
WebSocketMessage(
id="Main", type="Update", data={"PowerSign": Config.power_sign}
).model_dump()
)
if len(self.task_dict) == 0 and Config.power_sign != "NoAction":
logger.info(f"所有任务已结束,准备执行电源操作: {Config.power_sign}")
await Config.send_json(
WebSocketMessage(
id="Main",
type="Message",
data={
"type": "Countdown",
"title": f"{POWER_SIGN_MAP[Config.power_sign]}倒计时",
"message": f"程序将在倒计时结束后执行 {POWER_SIGN_MAP[Config.power_sign]} 操作",
},
).model_dump()
)
await System.start_power_task()
async def start_startup_queue(self):
"""开始运行启动时运行的调度队列"""
logger.info("开始运行启动时任务")
for uid, queue in Config.QueueConfig.items():
if queue.get("Info", "StartUpEnabled") and uid not in self.task_dict:
logger.info(f"启动时需要运行的队列:{uid}")
task_id = await TaskManager.add_task("自动代理", str(uid))
await Config.send_json(
WebSocketMessage(
id="TaskManager", type="Signal", data={"newTask": str(task_id)}
).model_dump()
)
logger.success("启动时任务开始运行")
TaskManager = _TaskManager()

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

@@ -0,0 +1,147 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import asyncio
import keyboard
from datetime import datetime
from app.services import Matomo, System
from app.utils import get_logger
from app.models.schema import WebSocketMessage
from .config import Config, QueueConfig
from .task_manager import TaskManager
logger = get_logger("主业务定时器")
class _MainTimer:
async def second_task(self):
"""每秒定期任务"""
logger.info("每秒定期任务启动")
while True:
await self.set_silence()
await self.timed_start()
await asyncio.sleep(1)
async def hour_task(self):
"""每小时定期任务"""
logger.info("每小时定期任务启动")
while True:
if (
datetime.strptime(
Config.get("Data", "LastStatisticsUpload"), "%Y-%m-%d %H:%M:%S"
).date()
!= datetime.now().date()
):
await Matomo.send_event(
"App",
"Version",
Config.version(),
1 if "beta" in Config.version() else 0,
)
await Config.set(
"Data",
"LastStatisticsUpload",
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
)
await asyncio.sleep(3600)
@logger.catch()
async def timed_start(self):
"""定时启动代理任务"""
curtime = datetime.now().strftime("%Y-%m-%d %H:%M")
for uid, queue in Config.QueueConfig.items():
if not queue.get("Info", "TimeEnabled"):
continue
# 避免重复调起任务
if curtime == queue.get("Data", "LastTimedStart"):
continue
for time_set in queue.TimeSet.values():
if (
time_set.get("Info", "Enabled")
and curtime[11:16] == time_set.get("Info", "Time")
and uid not in Config.task_dict
):
logger.info(f"定时唤起任务:{uid}")
task_id = await TaskManager.add_task("自动代理", str(uid))
await queue.set("Data", "LastTimedStart", curtime)
await Config.QueueConfig.save()
await Config.send_json(
WebSocketMessage(
id="TaskManager",
type="Signal",
data={"newTask": str(task_id)},
).model_dump()
)
@logger.catch()
async def set_silence(self):
"""静默模式通过模拟老板键来隐藏模拟器窗口"""
if (
len(Config.if_ignore_silence) == 0
and Config.get("Function", "IfSilence")
and Config.get("Function", "BossKey") != ""
):
windows = await System.get_window_info()
emulator_windows = []
for window in windows:
for emulator_path, endtime in Config.silence_dict.items():
if (
datetime.now() < endtime
and str(emulator_path) in window
and window[0] != "新通知" # 此处排除雷电名为新通知的窗口
):
emulator_windows.append(window)
if emulator_windows:
logger.info(f"检测到模拟器窗口: {emulator_windows}")
try:
keyboard.press_and_release(
"+".join(
_.strip().lower()
for _ in Config.get("Function", "BossKey").split("+")
)
)
logger.info(f"模拟按键: {Config.get('Function', 'BossKey')}")
except Exception as e:
logger.exception(f"模拟按键时出错: {e}")
MainTimer = _MainTimer()

934
app/models/ConfigBase.py Normal file
View File

@@ -0,0 +1,934 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import json
import uuid
import win32com.client
from copy import deepcopy
from urllib.parse import urlparse
from datetime import datetime
from pathlib import Path
from typing import List, Any, Dict, Union, Optional, TypeVar, Generic, Type
from app.utils import dpapi_encrypt, dpapi_decrypt
from app.utils.constants import RESERVED_NAMES, ILLEGAL_CHARS, DEFAULT_DATETIME
class ConfigValidator:
"""基础配置验证器"""
def validate(self, value: Any) -> bool:
"""验证值是否合法"""
return True
def correct(self, value: Any) -> Any:
"""修正非法值"""
return value
class RangeValidator(ConfigValidator):
"""范围验证器"""
def __init__(self, min: int | float, max: int | float):
self.min = min
self.max = max
self.range = (min, max)
def validate(self, value: Any) -> bool:
if not isinstance(value, (int | float)):
return False
return self.min <= value <= self.max
def correct(self, value: Any) -> int | float:
if not isinstance(value, (int, float)):
try:
value = float(value)
except TypeError:
return self.min
return min(max(self.min, value), self.max)
class OptionsValidator(ConfigValidator):
"""选项验证器"""
def __init__(self, options: list):
if not options:
raise ValueError("可选项不能为空")
self.options = options
def validate(self, value: Any) -> bool:
return value in self.options
def correct(self, value: Any) -> Any:
return value if self.validate(value) else self.options[0]
class UUIDValidator(ConfigValidator):
"""UUID验证器"""
def validate(self, value: Any) -> bool:
try:
uuid.UUID(value)
return True
except (TypeError, ValueError):
return False
def correct(self, value: Any) -> Any:
return value if self.validate(value) else str(uuid.uuid4())
class DateTimeValidator(ConfigValidator):
"""日期时间验证器"""
def __init__(self, date_format: str) -> None:
if not date_format:
raise ValueError("日期时间格式不能为空")
self.date_format = date_format
def validate(self, value: Any) -> bool:
if not isinstance(value, str):
return False
try:
datetime.strptime(value, self.date_format)
return True
except ValueError:
return False
def correct(self, value: Any) -> str:
if not isinstance(value, str):
return DEFAULT_DATETIME.strftime(self.date_format)
try:
datetime.strptime(value, self.date_format)
return value
except ValueError:
return DEFAULT_DATETIME.strftime(self.date_format)
class JSONValidator(ConfigValidator):
def __init__(self, tpye: type[dict] | type[list] = dict) -> None:
self.type = tpye
def validate(self, value: Any) -> bool:
if not isinstance(value, str):
return False
try:
data = json.loads(value)
if isinstance(data, self.type):
return True
else:
return False
except json.JSONDecodeError:
return False
def correct(self, value: Any) -> str:
return (
value if self.validate(value) else ("{ }" if self.type == dict else "[ ]")
)
class EncryptValidator(ConfigValidator):
"""加密数据验证器"""
def validate(self, value: Any) -> bool:
if not isinstance(value, str):
return False
try:
dpapi_decrypt(value)
return True
except:
return False
def correct(self, value: Any) -> Any:
return value if self.validate(value) else dpapi_encrypt("数据损坏, 请重新设置")
class BoolValidator(OptionsValidator):
"""布尔值验证器"""
def __init__(self):
super().__init__([True, False])
class FileValidator(ConfigValidator):
"""文件路径验证器"""
def validate(self, value: Any) -> bool:
if not isinstance(value, str):
return False
if not Path(value).is_absolute():
return False
if Path(value).suffix == ".lnk":
return False
return True
def correct(self, value: Any) -> str:
if not isinstance(value, str):
value = str(Path.cwd())
if not Path(value).is_absolute():
value = Path(value).resolve().as_posix()
if Path(value).suffix == ".lnk":
try:
shell = win32com.client.Dispatch("WScript.Shell")
shortcut = shell.CreateShortcut(value)
value = shortcut.TargetPath
except:
pass
return Path(value).resolve().as_posix()
class FolderValidator(ConfigValidator):
"""文件夹路径验证器"""
def validate(self, value: Any) -> bool:
if not isinstance(value, str):
return False
if not Path(value).is_absolute():
return False
return True
def correct(self, value: Any) -> str:
if not isinstance(value, str):
value = str(Path.cwd())
return Path(value).resolve().as_posix()
class UserNameValidator(ConfigValidator):
"""用户名验证器"""
def validate(self, value: Any) -> bool:
if not isinstance(value, str):
return False
if not value or not value.strip():
return False
if value != value.strip() or value != value.strip("."):
return False
if any(char in ILLEGAL_CHARS for char in value):
return False
if value.upper() in RESERVED_NAMES:
return False
if len(value) > 255:
return False
return True
def correct(self, value: Any) -> str:
if not isinstance(value, str):
value = "默认用户名"
value = value.strip().strip(".")
value = "".join(char for char in value if char not in ILLEGAL_CHARS)
if value.upper() in RESERVED_NAMES or not value:
value = "默认用户名"
if len(value) > 255:
value = value[:255]
return value
class URLValidator(ConfigValidator):
"""URL格式验证器"""
def __init__(
self,
schemes: list[str] | None = None,
require_netloc: bool = True,
default: str = "",
):
"""
:param schemes: 允许的协议列表, 若为 None 则允许任意协议
:param require_netloc: 是否要求必须包含网络位置, 如域名或IP
"""
self.schemes = [s.lower() for s in schemes] if schemes else None
self.require_netloc = require_netloc
self.default = default
def validate(self, value: Any) -> bool:
if value == self.default:
return True
if not isinstance(value, str):
return False
try:
parsed = urlparse(value)
except Exception:
return False
# 检查协议
if self.schemes is not None:
if not parsed.scheme or parsed.scheme.lower() not in self.schemes:
return False
else:
# 不限制协议仍要求有 scheme
if not parsed.scheme:
return False
# 检查是否包含网络位置
if self.require_netloc and not parsed.netloc:
return False
return True
def correct(self, value: Any) -> str:
if self.validate(value):
return value
if isinstance(value, str):
# 简单尝试:若看起来像域名,加上 https://
stripped = value.strip()
if stripped and not stripped.startswith(("http://", "https://")):
candidate = f"https://{stripped}"
if self.validate(candidate):
return candidate
return self.default
class ConfigItem:
"""配置项"""
def __init__(
self,
group: str,
name: str,
default: Any,
validator: Optional[ConfigValidator] = None,
):
"""
Parameters
----------
group: str
配置项分组名称
name: str
配置项字段名称
default: Any
配置项默认值
validator: ConfigValidator
配置项验证器, 默认为 None, 表示不进行验证
"""
super().__init__()
self.group = group
self.name = name
self.value: Any = default
self.validator = validator or ConfigValidator()
self.is_locked = False
if not self.validator.validate(self.value):
raise ValueError(
f"配置项 '{self.group}.{self.name}' 的默认值 '{self.value}' 不合法"
)
def setValue(self, value: Any):
"""
设置配置项值, 将自动进行验证和修正
Parameters
----------
value: Any
要设置的值, 可以是任何合法类型
"""
if (
dpapi_decrypt(self.value)
if isinstance(self.validator, EncryptValidator)
else self.value
) == value:
return
if self.is_locked:
raise ValueError(f"配置项 '{self.group}.{self.name}' 已锁定, 无法修改")
# deepcopy new value
try:
self.value = deepcopy(value)
except:
self.value = value
if isinstance(self.validator, EncryptValidator):
if self.validator.validate(self.value):
self.value = self.value
else:
self.value = dpapi_encrypt(self.value)
if not self.validator.validate(self.value):
self.value = self.validator.correct(self.value)
def getValue(self, if_decrypt: bool = True) -> Any:
"""
获取配置项值
"""
v = (
self.value
if self.validator.validate(self.value)
else self.validator.correct(self.value)
)
if isinstance(self.validator, EncryptValidator) and if_decrypt:
return dpapi_decrypt(v)
return v
def lock(self):
"""
锁定配置项, 锁定后无法修改配置项值
"""
self.is_locked = True
def unlock(self):
"""
解锁配置项, 解锁后可以修改配置项值
"""
self.is_locked = False
class ConfigBase:
"""
配置基类
这个类提供了基本的配置项管理功能, 包括连接配置文件、加载配置数据、获取和设置配置项值等。
此类不支持直接实例化, 必须通过子类来实现具体的配置项, 请继承此类并在子类中定义具体的配置项。
若将配置项设为类属性, 则所有实例都会共享同一份配置项数据。
若将配置项设为实例属性, 则每个实例都会有独立的配置项数据。
子配置项可以是 `MultipleConfig` 的实例。
"""
def __init__(self, if_save_multi_config: bool = True):
self.file: Optional[Path] = None
self.if_save_multi_config = if_save_multi_config
self.is_locked = False
async def connect(self, path: Path):
"""
将配置文件连接到指定配置文件
Parameters
----------
path: Path
配置文件路径, 必须为 JSON 文件, 如果不存在则会创建
"""
if path.suffix != ".json":
raise ValueError("配置文件必须是扩展名为 '.json' 的 JSON 文件")
if self.is_locked:
raise ValueError("配置已锁定, 无法修改")
self.file = path
if not self.file.exists():
self.file.parent.mkdir(parents=True, exist_ok=True)
self.file.touch()
try:
data = json.loads(self.file.read_text(encoding="utf-8"))
except json.JSONDecodeError:
data = {}
await self.load(data)
async def load(self, data: dict):
"""
从字典加载配置数据
这个方法会遍历字典中的配置项, 并将其设置到对应的 ConfigItem 实例中。
如果字典中包含 "SubConfigsInfo" 键, 则会加载子配置项, 这些子配置项应该是 MultipleConfig 的实例。
Parameters
----------
data: dict
配置数据字典
"""
if self.is_locked:
raise ValueError("配置已锁定, 无法修改")
# update the value of config item
if data.get("SubConfigsInfo"):
for k, v in data["SubConfigsInfo"].items():
if hasattr(self, k):
sub_config = getattr(self, k)
if isinstance(sub_config, MultipleConfig):
await sub_config.load(v)
data.pop("SubConfigsInfo")
for group, info in data.items():
for name, value in info.items():
if hasattr(self, f"{group}_{name}"):
configItem = getattr(self, f"{group}_{name}")
if isinstance(configItem, ConfigItem):
configItem.setValue(value)
if self.file:
await self.save()
async def toDict(
self, ignore_multi_config: bool = False, if_decrypt: bool = True
) -> Dict[str, Any]:
"""将配置项转换为字典"""
data = {}
for name in dir(self):
item = getattr(self, name)
if isinstance(item, ConfigItem):
if not data.get(item.group):
data[item.group] = {}
if item.name:
data[item.group][item.name] = item.getValue(if_decrypt)
elif not ignore_multi_config and isinstance(item, MultipleConfig):
if not data.get("SubConfigsInfo"):
data["SubConfigsInfo"] = {}
data["SubConfigsInfo"][name] = await item.toDict()
return data
def get(self, group: str, name: str) -> Any:
"""获取配置项的值"""
if not hasattr(self, f"{group}_{name}"):
raise AttributeError(f"配置项 '{group}.{name}' 不存在")
configItem = getattr(self, f"{group}_{name}")
if isinstance(configItem, ConfigItem):
return configItem.getValue()
else:
raise TypeError(f"配置项 '{group}.{name}' 不是 ConfigItem 实例")
async def set(self, group: str, name: str, value: Any):
"""
设置配置项的值
Parameters
----------
group: str
配置项分组名称
name: str
配置项名称
value: Any
配置项新值
"""
if not hasattr(self, f"{group}_{name}"):
raise AttributeError(f"配置项 '{group}.{name}' 不存在")
if self.is_locked:
raise ValueError("配置已锁定, 无法修改")
configItem = getattr(self, f"{group}_{name}")
if isinstance(configItem, ConfigItem):
configItem.setValue(value)
if self.file:
await self.save()
else:
raise TypeError(f"配置项 '{group}.{name}' 不是 ConfigItem 实例")
async def save(self):
"""保存配置"""
if not self.file:
raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件")
self.file.parent.mkdir(parents=True, exist_ok=True)
self.file.write_text(
json.dumps(
await self.toDict(not self.if_save_multi_config, if_decrypt=False),
ensure_ascii=False,
indent=4,
),
encoding="utf-8",
)
async def lock(self):
"""
锁定配置项, 锁定后无法修改配置项值
"""
self.is_locked = True
for name in dir(self):
item = getattr(self, name)
if isinstance(item, ConfigItem):
item.lock()
elif isinstance(item, MultipleConfig):
await item.lock()
async def unlock(self):
"""
解锁配置项, 解锁后可以修改配置项值
"""
self.is_locked = False
for name in dir(self):
item = getattr(self, name)
if isinstance(item, ConfigItem):
item.unlock()
elif isinstance(item, MultipleConfig):
await item.unlock()
T = TypeVar("T", bound="ConfigBase")
class MultipleConfig(Generic[T]):
"""
多配置项管理类
这个类允许管理多个配置项实例, 可以添加、删除、修改配置项, 并将其保存到 JSON 文件中。
允许通过 `config[uuid]` 访问配置项, 使用 `uuid in config` 检查是否存在配置项, 使用 `len(config)` 获取配置项数量。
Parameters
----------
sub_config_type: List[type]
子配置项的类型列表, 必须是 ConfigBase 的子类
"""
def __init__(self, sub_config_type: List[Type[T]]):
if not sub_config_type:
raise ValueError("子配置项类型列表不能为空")
for config_type in sub_config_type:
if not issubclass(config_type, ConfigBase):
raise TypeError(
f"配置类型 {config_type.__name__} 必须是 ConfigBase 的子类"
)
self.sub_config_type: List[Type[T]] = sub_config_type
self.file: Path | None = None
self.order: List[uuid.UUID] = []
self.data: Dict[uuid.UUID, T] = {}
self.is_locked = False
def __getitem__(self, key: uuid.UUID) -> T:
"""允许通过 config[uuid] 访问配置项"""
if key not in self.data:
raise KeyError(f"配置项 '{key}' 不存在")
return self.data[key]
def __contains__(self, key: uuid.UUID) -> bool:
"""允许使用 uuid in config 检查是否存在"""
return key in self.data
def __len__(self) -> int:
"""允许使用 len(config) 获取配置项数量"""
return len(self.data)
def __repr__(self) -> str:
"""更好的字符串表示"""
return f"MultipleConfig(items={len(self.data)}, types={[t.__name__ for t in self.sub_config_type]})"
def __str__(self) -> str:
"""用户友好的字符串表示"""
return f"MultipleConfig with {len(self.data)} items"
async def connect(self, path: Path):
"""
将配置文件连接到指定配置文件
Parameters
----------
path: Path
配置文件路径, 必须为 JSON 文件, 如果不存在则会创建
"""
if path.suffix != ".json":
raise ValueError("配置文件必须是带有 '.json' 扩展名的 JSON 文件。")
if self.is_locked:
raise ValueError("配置已锁定, 无法修改")
self.file = path
if not self.file.exists():
self.file.parent.mkdir(parents=True, exist_ok=True)
self.file.touch()
try:
data = json.loads(self.file.read_text(encoding="utf-8"))
except json.JSONDecodeError:
data = {}
await self.load(data)
async def load(self, data: dict):
"""
从字典加载配置数据
这个方法会遍历字典中的配置项, 并将其设置到对应的 ConfigBase 实例中。
如果字典中包含 "instances" 键, 则会加载子配置项, 这些子配置项应该是 ConfigBase 子类的实例。
如果字典中没有 "instances" 键, 则清空当前配置项。
Parameters
----------
data: dict
配置数据字典
"""
if self.is_locked:
raise ValueError("配置已锁定, 无法修改")
if not data.get("instances"):
self.order = []
self.data = {}
return
self.order = []
self.data = {}
for instance in data["instances"]:
if not isinstance(instance, dict) or not data.get(instance.get("uid")):
continue
type_name = instance.get("type", self.sub_config_type[0].__name__)
for class_type in self.sub_config_type:
if class_type.__name__ == type_name:
self.order.append(uuid.UUID(instance["uid"]))
self.data[self.order[-1]] = class_type()
await self.data[self.order[-1]].load(data[instance["uid"]])
break
else:
raise ValueError(f"未知的子配置类型: {type_name}")
if self.file:
await self.save()
async def toDict(
self, ignore_multi_config: bool = False, if_decrypt: bool = True
) -> Dict[str, Union[list, dict]]:
"""
将配置项转换为字典
返回一个字典, 包含所有配置项的 UID 和类型, 以及每个配置项的具体数据。
"""
data: Dict[str, Union[list, dict]] = {
"instances": [
{"uid": str(_), "type": type(self.data[_]).__name__} for _ in self.order
]
}
for uid, config in self.items():
data[str(uid)] = await config.toDict(ignore_multi_config, if_decrypt)
return data
async def get(self, uid: uuid.UUID) -> Dict[str, Union[list, dict]]:
"""
获取指定 UID 的配置项
Parameters
----------
uid: uuid.UUID
要获取的配置项的唯一标识符
Returns
-------
Dict[str, Union[list, dict]]
对应的配置项数据字典
"""
if uid not in self.data:
raise ValueError(f"配置项 '{uid}' 不存在。")
data: Dict[str, Union[list, dict]] = {
"instances": [
{"uid": str(_), "type": type(self.data[_]).__name__}
for _ in self.order
if _ == uid
]
}
data[str(uid)] = await self.data[uid].toDict()
return data
async def save(self):
"""保存配置"""
if not self.file:
raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件")
self.file.parent.mkdir(parents=True, exist_ok=True)
self.file.write_text(
json.dumps(await self.toDict(), ensure_ascii=False, indent=4),
encoding="utf-8",
)
async def add(self, config_type: Type[T]) -> tuple[uuid.UUID, T]:
"""
添加一个新的配置项
Parameters
----------
config_type: type
配置项的类型, 必须是初始化时已声明的 ConfigBase 子类
Returns
-------
tuple[uuid.UUID, ConfigBase]
新创建的配置项的唯一标识符和实例
"""
if config_type not in self.sub_config_type:
raise ValueError(f"配置类型 {config_type.__name__} 不被允许")
uid = uuid.uuid4()
self.order.append(uid)
self.data[uid] = config_type()
if self.file:
await self.save()
return uid, self.data[uid]
async def remove(self, uid: uuid.UUID):
"""
移除配置项
Parameters
----------
uid: uuid.UUID
要移除的配置项的唯一标识符
"""
if self.is_locked:
raise ValueError("配置已锁定, 无法修改")
if uid not in self.data:
raise ValueError(f"配置项 '{uid}' 不存在")
if self.data[uid].is_locked:
raise ValueError(f"配置项 '{uid}' 已锁定, 无法移除")
self.data.pop(uid)
self.order.remove(uid)
if self.file:
await self.save()
async def setOrder(self, order: List[uuid.UUID]):
"""
设置配置项的顺序
Parameters
----------
order: List[uuid.UUID]
新的配置项顺序
"""
if set(order) != set(self.data.keys()):
raise ValueError("顺序与当前配置项不匹配")
self.order = order
if self.file:
await self.save()
async def lock(self):
"""
锁定配置项, 锁定后无法修改配置项值
"""
self.is_locked = True
for item in self.values():
await item.lock()
async def unlock(self):
"""
解锁配置项, 解锁后可以修改配置项值
"""
self.is_locked = False
for item in self.values():
await item.unlock()
def keys(self):
"""返回配置项的所有唯一标识符"""
return iter(self.order)
def values(self):
"""返回配置项的所有实例"""
if not self.data:
return iter([])
return iter([self.data[_] for _ in self.order])
def items(self):
"""返回配置项的所有唯一标识符和实例的元组"""
return zip(self.keys(), self.values())
class MultipleUIDValidator(ConfigValidator):
"""多配置管理类UID验证器"""
def __init__(
self, default: Any, related_config: Dict[str, MultipleConfig], config_name: str
):
self.default = default
self.related_config = related_config
self.config_name = config_name
def validate(self, value: Any) -> bool:
if value == self.default:
return True
if not isinstance(value, str):
return False
try:
uid = uuid.UUID(value)
except (TypeError, ValueError):
return False
if uid in self.related_config.get(self.config_name, {}):
return True
return False
def correct(self, value: Any) -> Any:
if self.validate(value):
return value
return self.default

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

@@ -0,0 +1,31 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
__version__ = "5.0.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .ConfigBase import *
from .config import *
from .schema import *
__all__ = ["ConfigBase", "config", "schema"]

569
app/models/config.py Normal file
View File

@@ -0,0 +1,569 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
from pathlib import Path
from datetime import datetime, timedelta
from .ConfigBase import *
class Webhook(ConfigBase):
"""Webhook 配置"""
Info_Name = ConfigItem("Info", "Name", "")
Info_Enabled = ConfigItem("Info", "Enabled", True, BoolValidator())
Data_Url = ConfigItem("Data", "Url", "", URLValidator())
Data_Template = ConfigItem("Data", "Template", "")
Data_Headers = ConfigItem("Data", "Headers", "{ }", JSONValidator())
Data_Method = ConfigItem(
"Data", "Method", "POST", OptionsValidator(["POST", "GET"])
)
class GlobalConfig(ConfigBase):
"""全局配置"""
Function_HistoryRetentionTime = ConfigItem(
"Function",
"HistoryRetentionTime",
0,
OptionsValidator([7, 15, 30, 60, 90, 180, 365, 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()
)
Function_IfSkipMumuSplashAds = ConfigItem(
"Function", "IfSkipMumuSplashAds", False, BoolValidator()
)
Voice_Enabled = ConfigItem("Voice", "Enabled", False, BoolValidator())
Voice_Type = ConfigItem(
"Voice", "Type", "simple", OptionsValidator(["simple", "noisy"])
)
Start_IfSelfStart = ConfigItem("Start", "IfSelfStart", False, BoolValidator())
Start_IfMinimizeDirectly = ConfigItem(
"Start", "IfMinimizeDirectly", False, BoolValidator()
)
UI_IfShowTray = ConfigItem("UI", "IfShowTray", False, BoolValidator())
UI_IfToTray = ConfigItem("UI", "IfToTray", False, BoolValidator())
Notify_SendTaskResultTime = ConfigItem(
"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", "", EncryptValidator()
)
Notify_FromAddress = ConfigItem("Notify", "FromAddress", "")
Notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
Notify_IfServerChan = ConfigItem("Notify", "IfServerChan", False, BoolValidator())
Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
Notify_CustomWebhooks = MultipleConfig([Webhook])
Update_IfAutoUpdate = ConfigItem("Update", "IfAutoUpdate", False, BoolValidator())
Update_Source = ConfigItem(
"Update",
"Source",
"GitHub",
OptionsValidator(["GitHub", "MirrorChyan", "AutoSite"]),
)
Update_ProxyAddress = ConfigItem("Update", "ProxyAddress", "")
Update_MirrorChyanCDK = ConfigItem(
"Update", "MirrorChyanCDK", "", EncryptValidator()
)
Data_UID = ConfigItem("Data", "UID", str(uuid.uuid4()), UUIDValidator())
Data_LastStatisticsUpload = ConfigItem(
"Data",
"LastStatisticsUpload",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
)
Data_LastStageUpdated = ConfigItem(
"Data",
"LastStageUpdated",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
)
Data_StageTimeStamp = ConfigItem(
"Data",
"StageTimeStamp",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
)
Data_Stage = ConfigItem("Data", "Stage", "{ }", JSONValidator())
Data_LastNoticeUpdated = ConfigItem(
"Data",
"LastNoticeUpdated",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
)
Data_IfShowNotice = ConfigItem("Data", "IfShowNotice", True, BoolValidator())
Data_Notice = ConfigItem("Data", "Notice", "{ }", JSONValidator())
Data_LastWebConfigUpdated = ConfigItem(
"Data",
"LastWebConfigUpdated",
"2000-01-01 00:00:00",
DateTimeValidator("%Y-%m-%d %H:%M:%S"),
)
Data_WebConfig = ConfigItem("Data", "WebConfig", "{ }", JSONValidator())
class QueueItem(ConfigBase):
"""队列项配置"""
related_config: dict[str, MultipleConfig] = {}
def __init__(self) -> None:
super().__init__()
self.Info_ScriptId = ConfigItem(
"Info",
"ScriptId",
"-",
MultipleUIDValidator("-", self.related_config, "ScriptConfig"),
)
class TimeSet(ConfigBase):
"""时间设置配置"""
def __init__(self) -> None:
super().__init__()
self.Info_Enabled = ConfigItem("Info", "Enabled", False, BoolValidator())
self.Info_Time = ConfigItem("Info", "Time", "00:00", DateTimeValidator("%H:%M"))
class QueueConfig(ConfigBase):
"""队列配置"""
def __init__(self) -> None:
super().__init__()
self.Info_Name = ConfigItem("Info", "Name", "新队列")
self.Info_TimeEnabled = ConfigItem(
"Info", "TimeEnabled", False, BoolValidator()
)
self.Info_StartUpEnabled = ConfigItem(
"Info", "StartUpEnabled", False, BoolValidator()
)
self.Info_AfterAccomplish = ConfigItem(
"Info",
"AfterAccomplish",
"NoAction",
OptionsValidator(
[
"NoAction",
"KillSelf",
"Sleep",
"Hibernate",
"Shutdown",
"ShutdownForce",
]
),
)
self.Data_LastTimedStart = ConfigItem(
"Data",
"LastTimedStart",
"2000-01-01 00:00",
DateTimeValidator("%Y-%m-%d %H:%M"),
)
self.TimeSet = MultipleConfig([TimeSet])
self.QueueItem = MultipleConfig([QueueItem])
class MaaUserConfig(ConfigBase):
"""MAA用户配置"""
related_config: dict[str, MultipleConfig] = {}
def __init__(self) -> None:
super().__init__()
self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator())
self.Info_Id = ConfigItem("Info", "Id", "")
self.Info_Mode = ConfigItem(
"Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"])
)
self.Info_StageMode = ConfigItem(
"Info",
"StageMode",
"Fixed",
MultipleUIDValidator("Fixed", self.related_config, "PlanConfig"),
)
self.Info_Server = ConfigItem(
"Info",
"Server",
"Official",
OptionsValidator(
["Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy"]
),
)
self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator())
self.Info_RemainedDay = ConfigItem(
"Info", "RemainedDay", -1, RangeValidator(-1, 9999)
)
self.Info_Annihilation = ConfigItem(
"Info",
"Annihilation",
"Annihilation",
OptionsValidator(
[
"Close",
"Annihilation",
"Chernobog@Annihilation",
"LungmenOutskirts@Annihilation",
"LungmenDowntown@Annihilation",
]
),
)
self.Info_Routine = ConfigItem("Info", "Routine", True, BoolValidator())
self.Info_InfrastMode = ConfigItem(
"Info",
"InfrastMode",
"Normal",
OptionsValidator(["Normal", "Rotation", "Custom"]),
)
self.Info_InfrastPath = ConfigItem(
"Info", "InfrastPath", str(Path.cwd()), FileValidator()
)
self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator())
self.Info_Notes = ConfigItem("Info", "Notes", "")
self.Info_MedicineNumb = ConfigItem(
"Info", "MedicineNumb", 0, RangeValidator(0, 9999)
)
self.Info_SeriesNumb = ConfigItem(
"Info",
"SeriesNumb",
"0",
OptionsValidator(["0", "6", "5", "4", "3", "2", "1", "-1"]),
)
self.Info_Stage = ConfigItem("Info", "Stage", "-")
self.Info_Stage_1 = ConfigItem("Info", "Stage_1", "-")
self.Info_Stage_2 = ConfigItem("Info", "Stage_2", "-")
self.Info_Stage_3 = ConfigItem("Info", "Stage_3", "-")
self.Info_Stage_Remain = ConfigItem("Info", "Stage_Remain", "-")
self.Info_IfSkland = ConfigItem("Info", "IfSkland", False, BoolValidator())
self.Info_SklandToken = ConfigItem(
"Info", "SklandToken", "", EncryptValidator()
)
self.Data_LastProxyDate = ConfigItem(
"Data", "LastProxyDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
)
self.Data_LastAnnihilationDate = ConfigItem(
"Data", "LastAnnihilationDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
)
self.Data_LastSklandDate = ConfigItem(
"Data", "LastSklandDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
)
self.Data_ProxyTimes = ConfigItem(
"Data", "ProxyTimes", 0, RangeValidator(0, 9999)
)
self.Data_IfPassCheck = ConfigItem("Data", "IfPassCheck", True, BoolValidator())
self.Data_CustomInfrastPlanIndex = ConfigItem(
"Data", "CustomInfrastPlanIndex", "0"
)
self.Task_IfWakeUp = ConfigItem("Task", "IfWakeUp", True, BoolValidator())
self.Task_IfRecruiting = ConfigItem(
"Task", "IfRecruiting", True, BoolValidator()
)
self.Task_IfBase = ConfigItem("Task", "IfBase", True, BoolValidator())
self.Task_IfCombat = ConfigItem("Task", "IfCombat", True, BoolValidator())
self.Task_IfMall = ConfigItem("Task", "IfMall", True, BoolValidator())
self.Task_IfMission = ConfigItem("Task", "IfMission", True, BoolValidator())
self.Task_IfAutoRoguelike = ConfigItem(
"Task", "IfAutoRoguelike", False, BoolValidator()
)
self.Task_IfReclamation = ConfigItem(
"Task", "IfReclamation", False, BoolValidator()
)
self.Notify_Enabled = ConfigItem("Notify", "Enabled", False, BoolValidator())
self.Notify_IfSendStatistic = ConfigItem(
"Notify", "IfSendStatistic", False, BoolValidator()
)
self.Notify_IfSendSixStar = ConfigItem(
"Notify", "IfSendSixStar", False, BoolValidator()
)
self.Notify_IfSendMail = ConfigItem(
"Notify", "IfSendMail", False, BoolValidator()
)
self.Notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
self.Notify_IfServerChan = ConfigItem(
"Notify", "IfServerChan", False, BoolValidator()
)
self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
self.Notify_CustomWebhooks = MultipleConfig([Webhook])
class MaaConfig(ConfigBase):
"""MAA配置"""
def __init__(self) -> None:
super().__init__()
self.Info_Name = ConfigItem("Info", "Name", "新 MAA 脚本")
self.Info_Path = ConfigItem("Info", "Path", str(Path.cwd()), FolderValidator())
self.Run_TaskTransitionMethod = ConfigItem(
"Run",
"TaskTransitionMethod",
"ExitEmulator",
OptionsValidator(["NoAction", "ExitGame", "ExitEmulator"]),
)
self.Run_ProxyTimesLimit = ConfigItem(
"Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999)
)
self.Run_ADBSearchRange = ConfigItem(
"Run", "ADBSearchRange", 0, RangeValidator(0, 3)
)
self.Run_RunTimesLimit = ConfigItem(
"Run", "RunTimesLimit", 3, RangeValidator(1, 9999)
)
self.Run_AnnihilationTimeLimit = ConfigItem(
"Run", "AnnihilationTimeLimit", 40, RangeValidator(1, 9999)
)
self.Run_RoutineTimeLimit = ConfigItem(
"Run", "RoutineTimeLimit", 10, RangeValidator(1, 9999)
)
self.Run_AnnihilationWeeklyLimit = ConfigItem(
"Run", "AnnihilationWeeklyLimit", True, BoolValidator()
)
self.UserData = MultipleConfig([MaaUserConfig])
class MaaPlanConfig(ConfigBase):
"""MAA计划表配置"""
def __init__(self) -> None:
super().__init__()
self.Info_Name = ConfigItem("Info", "Name", "新 MAA 计划表")
self.Info_Mode = ConfigItem(
"Info", "Mode", "ALL", OptionsValidator(["ALL", "Weekly"])
)
self.config_item_dict: dict[str, Dict[str, ConfigItem]] = {}
for group in [
"ALL",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
]:
self.config_item_dict[group] = {}
self.config_item_dict[group]["MedicineNumb"] = ConfigItem(
group, "MedicineNumb", 0, RangeValidator(0, 9999)
)
self.config_item_dict[group]["SeriesNumb"] = ConfigItem(
group,
"SeriesNumb",
"0",
OptionsValidator(["0", "6", "5", "4", "3", "2", "1", "-1"]),
)
self.config_item_dict[group]["Stage"] = ConfigItem(group, "Stage", "-")
self.config_item_dict[group]["Stage_1"] = ConfigItem(group, "Stage_1", "-")
self.config_item_dict[group]["Stage_2"] = ConfigItem(group, "Stage_2", "-")
self.config_item_dict[group]["Stage_3"] = ConfigItem(group, "Stage_3", "-")
self.config_item_dict[group]["Stage_Remain"] = ConfigItem(
group, "Stage_Remain", "-"
)
for name in [
"MedicineNumb",
"SeriesNumb",
"Stage",
"Stage_1",
"Stage_2",
"Stage_3",
"Stage_Remain",
]:
setattr(self, f"{group}_{name}", self.config_item_dict[group][name])
def get_current_info(self, name: str) -> ConfigItem:
"""获取当前的计划表配置项"""
if self.get("Info", "Mode") == "ALL":
return self.config_item_dict["ALL"][name]
elif self.get("Info", "Mode") == "Weekly":
dt = datetime.now()
if dt.time() < datetime.min.time().replace(hour=4):
dt = dt - timedelta(days=1)
today = dt.strftime("%A")
if today in self.config_item_dict:
return self.config_item_dict[today][name]
else:
return self.config_item_dict["ALL"][name]
else:
raise ValueError("非法的计划表模式")
class GeneralUserConfig(ConfigBase):
"""通用脚本用户配置"""
def __init__(self) -> None:
super().__init__()
self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator())
self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator())
self.Info_RemainedDay = ConfigItem(
"Info", "RemainedDay", -1, RangeValidator(-1, 9999)
)
self.Info_IfScriptBeforeTask = ConfigItem(
"Info", "IfScriptBeforeTask", False, BoolValidator()
)
self.Info_ScriptBeforeTask = ConfigItem(
"Info", "ScriptBeforeTask", str(Path.cwd()), FileValidator()
)
self.Info_IfScriptAfterTask = ConfigItem(
"Info", "IfScriptAfterTask", False, BoolValidator()
)
self.Info_ScriptAfterTask = ConfigItem(
"Info", "ScriptAfterTask", str(Path.cwd()), FileValidator()
)
self.Info_Notes = ConfigItem("Info", "Notes", "")
self.Data_LastProxyDate = ConfigItem(
"Data", "LastProxyDate", "2000-01-01", DateTimeValidator("%Y-%m-%d")
)
self.Data_ProxyTimes = ConfigItem(
"Data", "ProxyTimes", 0, RangeValidator(0, 9999)
)
self.Notify_Enabled = ConfigItem("Notify", "Enabled", False, BoolValidator())
self.Notify_IfSendStatistic = ConfigItem(
"Notify", "IfSendStatistic", False, BoolValidator()
)
self.Notify_IfSendMail = ConfigItem(
"Notify", "IfSendMail", False, BoolValidator()
)
self.Notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
self.Notify_IfServerChan = ConfigItem(
"Notify", "IfServerChan", False, BoolValidator()
)
self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
self.Notify_CustomWebhooks = MultipleConfig([Webhook])
class GeneralConfig(ConfigBase):
"""通用配置"""
def __init__(self) -> None:
super().__init__()
self.Info_Name = ConfigItem("Info", "Name", "新通用脚本")
self.Info_RootPath = ConfigItem(
"Info", "RootPath", str(Path.cwd()), FileValidator()
)
self.Script_ScriptPath = ConfigItem(
"Script", "ScriptPath", str(Path.cwd()), FileValidator()
)
self.Script_Arguments = ConfigItem("Script", "Arguments", "")
self.Script_IfTrackProcess = ConfigItem(
"Script", "IfTrackProcess", False, BoolValidator()
)
self.Script_ConfigPath = ConfigItem(
"Script", "ConfigPath", str(Path.cwd()), FileValidator()
)
self.Script_ConfigPathMode = ConfigItem(
"Script", "ConfigPathMode", "File", OptionsValidator(["File", "Folder"])
)
self.Script_UpdateConfigMode = ConfigItem(
"Script",
"UpdateConfigMode",
"Never",
OptionsValidator(["Never", "Success", "Failure", "Always"]),
)
self.Script_LogPath = ConfigItem(
"Script", "LogPath", str(Path.cwd()), FileValidator()
)
self.Script_LogPathFormat = ConfigItem("Script", "LogPathFormat", "%Y-%m-%d")
self.Script_LogTimeStart = ConfigItem(
"Script", "LogTimeStart", 1, RangeValidator(1, 9999)
)
self.Script_LogTimeEnd = ConfigItem(
"Script", "LogTimeEnd", 1, RangeValidator(1, 9999)
)
self.Script_LogTimeFormat = ConfigItem(
"Script", "LogTimeFormat", "%Y-%m-%d %H:%M:%S"
)
self.Script_SuccessLog = ConfigItem("Script", "SuccessLog", "")
self.Script_ErrorLog = ConfigItem("Script", "ErrorLog", "")
self.Game_Enabled = ConfigItem("Game", "Enabled", False, BoolValidator())
self.Game_Type = ConfigItem(
"Game", "Type", "Emulator", OptionsValidator(["Emulator", "Client"])
)
self.Game_Path = ConfigItem("Game", "Path", str(Path.cwd()), FileValidator())
self.Game_Arguments = ConfigItem("Game", "Arguments", "")
self.Game_WaitTime = ConfigItem("Game", "WaitTime", 0, RangeValidator(0, 9999))
self.Game_IfForceClose = ConfigItem(
"Game", "IfForceClose", False, BoolValidator()
)
self.Run_ProxyTimesLimit = ConfigItem(
"Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999)
)
self.Run_RunTimesLimit = ConfigItem(
"Run", "RunTimesLimit", 3, RangeValidator(1, 9999)
)
self.Run_RunTimeLimit = ConfigItem(
"Run", "RunTimeLimit", 10, RangeValidator(1, 9999)
)
self.UserData = MultipleConfig([GeneralUserConfig])
CLASS_BOOK = {"MAA": MaaConfig, "MaaPlan": MaaPlanConfig, "General": GeneralConfig}
"""配置类映射表"""

872
app/models/schema.py Normal file
View File

@@ -0,0 +1,872 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
from pydantic import BaseModel, Field
from typing import Any, Dict, List, Union, Optional, Literal
class OutBase(BaseModel):
code: int = Field(default=200, description="状态码")
status: str = Field(default="success", description="操作状态")
message: str = Field(default="操作成功", description="操作消息")
class InfoOut(OutBase):
data: Dict[str, Any] = Field(..., description="收到的服务器数据")
class VersionOut(OutBase):
if_need_update: bool = Field(..., description="后端代码是否需要更新")
current_hash: str = Field(..., description="后端代码当前哈希值")
current_time: str = Field(..., description="后端代码当前时间戳")
current_version: str = Field(..., description="后端当前版本号")
class NoticeOut(OutBase):
if_need_show: bool = Field(..., description="是否需要显示公告")
data: Dict[str, str] = Field(
..., description="公告信息, key为公告标题, value为公告内容"
)
class ComboBoxItem(BaseModel):
label: str = Field(..., description="展示值")
value: Optional[str] = Field(..., description="实际值")
class ComboBoxOut(OutBase):
data: List[ComboBoxItem] = Field(..., description="下拉框选项")
class GetStageIn(BaseModel):
type: Literal[
"Today",
"ALL",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
] = Field(
...,
description="选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项",
)
class WebhookIndexItem(BaseModel):
uid: str = Field(..., description="唯一标识符")
type: Literal["Webhook"] = Field(..., description="配置类型")
class Webhook_Info(BaseModel):
Name: Optional[str] = Field(default=None, description="Webhook名称")
Enabled: Optional[bool] = Field(default=None, description="是否启用")
class Webhook_Data(BaseModel):
Url: Optional[str] = Field(default=None, description="Webhook URL")
Template: Optional[str] = Field(default=None, description="消息模板")
Headers: Optional[Dict[str, str]] = Field(default=None, description="自定义请求头")
Method: Optional[Literal["POST", "GET"]] = Field(
default=None, description="请求方法"
)
class Webhook(BaseModel):
Info: Optional[Webhook_Info] = Field(default=None, description="Webhook基础信息")
Data: Optional[Webhook_Data] = Field(default=None, description="Webhook配置数据")
class GlobalConfig_Function(BaseModel):
HistoryRetentionTime: Optional[Literal[7, 15, 30, 60, 90, 180, 365, 0]] = Field(
None, description="历史记录保留时间, 0表示永久保存"
)
IfAllowSleep: Optional[bool] = Field(default=None, description="允许休眠")
IfSilence: Optional[bool] = Field(default=None, description="静默模式")
BossKey: Optional[str] = Field(default=None, description="模拟器老板键")
IfAgreeBilibili: Optional[bool] = Field(
default=None, description="同意哔哩哔哩用户协议"
)
IfSkipMumuSplashAds: Optional[bool] = Field(
default=None, description="跳过Mumu模拟器启动广告"
)
class GlobalConfig_Voice(BaseModel):
Enabled: Optional[bool] = Field(default=None, description="语音功能是否启用")
Type: Optional[Literal["simple", "noisy"]] = Field(
default=None, description="语音类型, simple为简洁, noisy为聒噪"
)
class GlobalConfig_Start(BaseModel):
IfSelfStart: Optional[bool] = Field(
default=None, description="是否在系统启动时自动运行"
)
IfMinimizeDirectly: Optional[bool] = Field(
default=None, description="启动时是否直接最小化到托盘而不显示主窗口"
)
class GlobalConfig_UI(BaseModel):
IfShowTray: Optional[bool] = Field(default=None, description="是否常态显示托盘图标")
IfToTray: Optional[bool] = Field(default=None, description="是否最小化到托盘")
class GlobalConfig_Notify(BaseModel):
SendTaskResultTime: Optional[Literal["不推送", "任何时刻", "仅失败时"]] = Field(
default=None, description="任务结果推送时机"
)
IfSendStatistic: Optional[bool] = Field(
default=None, description="是否发送统计信息"
)
IfSendSixStar: Optional[bool] = Field(
default=None, description="是否发送公招六星通知"
)
IfPushPlyer: Optional[bool] = Field(default=None, description="是否推送系统通知")
IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知")
SMTPServerAddress: Optional[str] = Field(default=None, description="SMTP服务器地址")
AuthorizationCode: Optional[str] = Field(default=None, description="SMTP授权码")
FromAddress: Optional[str] = Field(default=None, description="邮件发送地址")
ToAddress: Optional[str] = Field(default=None, description="邮件接收地址")
IfServerChan: Optional[bool] = Field(
default=None, description="是否使用ServerChan推送"
)
ServerChanKey: Optional[str] = Field(default=None, description="ServerChan推送密钥")
class GlobalConfig_Update(BaseModel):
IfAutoUpdate: Optional[bool] = Field(default=None, description="是否自动更新")
Source: Optional[Literal["GitHub", "MirrorChyan", "AutoSite"]] = Field(
default=None, description="更新源: GitHub源, Mirror酱源, 自建源"
)
ProxyAddress: Optional[str] = Field(default=None, description="网络代理地址")
MirrorChyanCDK: Optional[str] = Field(default=None, description="Mirror酱CDK")
class GlobalConfig(BaseModel):
Function: Optional[GlobalConfig_Function] = Field(
default=None, description="功能相关配置"
)
Voice: Optional[GlobalConfig_Voice] = Field(
default=None, description="语音相关配置"
)
Start: Optional[GlobalConfig_Start] = Field(
default=None, description="启动相关配置"
)
UI: Optional[GlobalConfig_UI] = Field(default=None, description="界面相关配置")
Notify: Optional[GlobalConfig_Notify] = Field(
default=None, description="通知相关配置"
)
Update: Optional[GlobalConfig_Update] = Field(
default=None, description="更新相关配置"
)
class QueueIndexItem(BaseModel):
uid: str = Field(..., description="唯一标识符")
type: Literal["QueueConfig"] = Field(..., description="配置类型")
class QueueItemIndexItem(BaseModel):
uid: str = Field(..., description="唯一标识符")
type: Literal["QueueItem"] = Field(..., description="配置类型")
class TimeSetIndexItem(BaseModel):
uid: str = Field(..., description="唯一标识符")
type: Literal["TimeSet"] = Field(..., description="配置类型")
class QueueItem_Info(BaseModel):
ScriptId: Optional[str] = Field(
default=None, description="任务所对应的脚本ID, 为None时表示未选择"
)
class QueueItem(BaseModel):
Info: Optional[QueueItem_Info] = Field(default=None, description="队列项")
class TimeSet_Info(BaseModel):
Enabled: Optional[bool] = Field(default=None, description="是否启用")
Time: Optional[str] = Field(default=None, description="时间设置, 格式为HH:MM")
class TimeSet(BaseModel):
Info: Optional[TimeSet_Info] = Field(default=None, description="时间项")
class QueueConfig_Info(BaseModel):
Name: Optional[str] = Field(default=None, description="队列名称")
TimeEnabled: Optional[bool] = Field(default=None, description="是否启用定时")
StartUpEnabled: Optional[bool] = Field(default=None, description="是否启动时运行")
AfterAccomplish: Optional[
Literal[
"NoAction", "KillSelf", "Sleep", "Hibernate", "Shutdown", "ShutdownForce"
]
] = Field(default=None, description="完成后操作")
class QueueConfig(BaseModel):
Info: Optional[QueueConfig_Info] = Field(default=None, description="队列信息")
class ScriptIndexItem(BaseModel):
uid: str = Field(..., description="唯一标识符")
type: Literal["MaaConfig", "GeneralConfig"] = Field(..., description="配置类型")
class UserIndexItem(BaseModel):
uid: str = Field(..., description="唯一标识符")
type: Literal["MaaUserConfig", "GeneralUserConfig"] = Field(
..., description="配置类型"
)
class MaaUserConfig_Info(BaseModel):
Name: Optional[str] = Field(default=None, description="用户名")
Id: Optional[str] = Field(default=None, description="用户ID")
Mode: Optional[Literal["简洁", "详细"]] = Field(
default=None, description="用户配置模式"
)
StageMode: Optional[str] = Field(default=None, description="关卡配置模式")
Server: Optional[
Literal["Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy"]
] = Field(default=None, description="服务器")
Status: Optional[bool] = Field(default=None, description="用户状态")
RemainedDay: Optional[int] = Field(default=None, description="剩余天数")
Annihilation: Optional[
Literal[
"Close",
"Annihilation",
"Chernobog@Annihilation",
"LungmenOutskirts@Annihilation",
"LungmenDowntown@Annihilation",
]
] = Field(default=None, description="剿灭模式")
Routine: Optional[bool] = Field(default=None, description="是否启用日常")
InfrastMode: Optional[Literal["Normal", "Rotation", "Custom"]] = Field(
default=None, description="基建模式"
)
InfrastPath: Optional[str] = Field(default=None, description="自定义基建文件路径")
Password: Optional[str] = Field(default=None, description="密码")
Notes: Optional[str] = Field(default=None, description="备注")
MedicineNumb: Optional[int] = Field(default=None, description="吃理智药数量")
SeriesNumb: Optional[Literal["0", "6", "5", "4", "3", "2", "1", "-1"]] = Field(
default=None, description="连战次数"
)
Stage: Optional[str] = Field(default=None, description="关卡选择")
Stage_1: Optional[str] = Field(default=None, description="备选关卡 - 1")
Stage_2: Optional[str] = Field(default=None, description="备选关卡 - 2")
Stage_3: Optional[str] = Field(default=None, description="备选关卡 - 3")
Stage_Remain: Optional[str] = Field(default=None, description="剩余理智关卡")
IfSkland: Optional[bool] = Field(default=None, description="是否启用森空岛签到")
SklandToken: Optional[str] = Field(default=None, description="SklandToken")
class MaaUserConfig_Data(BaseModel):
LastProxyDate: Optional[str] = Field(default=None, description="上次代理日期")
LastAnnihilationDate: Optional[str] = Field(
default=None, description="上次剿灭日期"
)
LastSklandDate: Optional[str] = Field(
default=None, description="上次森空岛签到日期"
)
ProxyTimes: Optional[int] = Field(default=None, description="代理次数")
IfPassCheck: Optional[bool] = Field(default=None, description="是否通过人工排查")
class MaaUserConfig_Task(BaseModel):
IfWakeUp: Optional[bool] = Field(default=None, description="开始唤醒")
IfRecruiting: Optional[bool] = Field(default=None, description="自动公招")
IfBase: Optional[bool] = Field(default=None, description="基建换班")
IfCombat: Optional[bool] = Field(default=None, description="刷理智")
IfMall: Optional[bool] = Field(default=None, description="获取信用及购物")
IfMission: Optional[bool] = Field(default=None, description="领取奖励")
IfAutoRoguelike: Optional[bool] = Field(default=None, description="自动肉鸽")
IfReclamation: Optional[bool] = Field(default=None, description="生息演算")
class MaaUserConfig_Notify(BaseModel):
Enabled: Optional[bool] = Field(default=None, description="是否启用通知")
IfSendStatistic: Optional[bool] = Field(
default=None, description="是否发送统计信息"
)
IfSendSixStar: Optional[bool] = Field(default=None, description="是否发送高资喜报")
IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知")
ToAddress: Optional[str] = Field(default=None, description="邮件接收地址")
IfServerChan: Optional[bool] = Field(
default=None, description="是否使用Server酱推送"
)
ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey")
class GeneralUserConfig_Notify(BaseModel):
Enabled: Optional[bool] = Field(default=None, description="是否启用通知")
IfSendStatistic: Optional[bool] = Field(
default=None, description="是否发送统计信息"
)
IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知")
ToAddress: Optional[str] = Field(default=None, description="邮件接收地址")
IfServerChan: Optional[bool] = Field(
default=None, description="是否使用Server酱推送"
)
ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey")
IfCompanyWebHookBot: Optional[bool] = Field(
default=None, description="是否使用Webhook推送"
)
CompanyWebHookBotUrl: Optional[str] = Field(
default=None, description="企微Webhook Bot URL"
)
class MaaUserConfig(BaseModel):
Info: Optional[MaaUserConfig_Info] = Field(default=None, description="基础信息")
Data: Optional[MaaUserConfig_Data] = Field(default=None, description="用户数据")
Task: Optional[MaaUserConfig_Task] = Field(default=None, description="任务列表")
Notify: Optional[MaaUserConfig_Notify] = Field(default=None, description="单独通知")
class MaaConfig_Info(BaseModel):
Name: Optional[str] = Field(default=None, description="脚本名称")
Path: Optional[str] = Field(default=None, description="脚本路径")
class MaaConfig_Run(BaseModel):
TaskTransitionMethod: Optional[Literal["NoAction", "ExitGame", "ExitEmulator"]] = (
Field(default=None, description="简洁任务间切换方式")
)
ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制")
ADBSearchRange: Optional[int] = Field(default=None, description="ADB端口搜索范围")
RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制")
AnnihilationTimeLimit: Optional[int] = Field(
default=None, description="剿灭超时限制"
)
RoutineTimeLimit: Optional[int] = Field(default=None, description="日常超时限制")
AnnihilationWeeklyLimit: Optional[bool] = Field(
default=None, description="剿灭每周仅代理至上限"
)
class MaaConfig(BaseModel):
Info: Optional[MaaConfig_Info] = Field(default=None, description="脚本基础信息")
Run: Optional[MaaConfig_Run] = Field(default=None, description="脚本运行配置")
class GeneralUserConfig_Info(BaseModel):
Name: Optional[str] = Field(default=None, description="用户名")
Status: Optional[bool] = Field(default=None, description="用户状态")
RemainedDay: Optional[int] = Field(default=None, description="剩余天数")
IfScriptBeforeTask: Optional[bool] = Field(
default=None, description="是否在任务前执行脚本"
)
ScriptBeforeTask: Optional[str] = Field(default=None, description="任务前脚本路径")
IfScriptAfterTask: Optional[bool] = Field(
default=None, description="是否在任务后执行脚本"
)
ScriptAfterTask: Optional[str] = Field(default=None, description="任务后脚本路径")
Notes: Optional[str] = Field(default=None, description="备注")
class GeneralUserConfig_Data(BaseModel):
LastProxyDate: Optional[str] = Field(default=None, description="上次代理日期")
ProxyTimes: Optional[int] = Field(default=None, description="代理次数")
class GeneralUserConfig(BaseModel):
Info: Optional[GeneralUserConfig_Info] = Field(default=None, description="用户信息")
Data: Optional[GeneralUserConfig_Data] = Field(default=None, description="用户数据")
Notify: Optional[GeneralUserConfig_Notify] = Field(
default=None, description="单独通知"
)
class GeneralConfig_Info(BaseModel):
Name: Optional[str] = Field(default=None, description="脚本名称")
RootPath: Optional[str] = Field(default=None, description="脚本根目录")
class GeneralConfig_Script(BaseModel):
ScriptPath: Optional[str] = Field(default=None, description="脚本可执行文件路径")
Arguments: Optional[str] = Field(default=None, description="脚本启动附加命令参数")
IfTrackProcess: Optional[bool] = Field(
default=None, description="是否追踪脚本子进程"
)
ConfigPath: Optional[str] = Field(default=None, description="配置文件路径")
ConfigPathMode: Optional[Literal["File", "Folder"]] = Field(
default=None, description="配置文件类型: 单个文件, 文件夹"
)
UpdateConfigMode: Optional[Literal["Never", "Success", "Failure", "Always"]] = (
Field(
default=None,
description="更新配置时机, 从不, 仅成功时, 仅失败时, 任务结束时",
)
)
LogPath: Optional[str] = Field(default=None, description="日志文件路径")
LogPathFormat: Optional[str] = Field(default=None, description="日志文件名格式")
LogTimeStart: Optional[int] = Field(default=None, description="日志时间戳开始位置")
LogTimeEnd: Optional[int] = Field(default=None, description="日志时间戳结束位置")
LogTimeFormat: Optional[str] = Field(default=None, description="日志时间戳格式")
SuccessLog: Optional[str] = Field(default=None, description="成功时日志")
ErrorLog: Optional[str] = Field(default=None, description="错误时日志")
class GeneralConfig_Game(BaseModel):
Enabled: Optional[bool] = Field(
default=None, description="游戏/模拟器相关功能是否启用"
)
Type: Optional[Literal["Emulator", "Client"]] = Field(
default=None, description="类型: 模拟器, PC端"
)
Path: Optional[str] = Field(default=None, description="游戏/模拟器程序路径")
Arguments: Optional[str] = Field(default=None, description="游戏/模拟器启动参数")
WaitTime: Optional[int] = Field(default=None, description="游戏/模拟器等待启动时间")
IfForceClose: Optional[bool] = Field(
default=None, description="是否强制关闭游戏/模拟器进程"
)
class GeneralConfig_Run(BaseModel):
ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制")
RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制")
RunTimeLimit: Optional[int] = Field(default=None, description="日志超时限制")
class GeneralConfig(BaseModel):
Info: Optional[GeneralConfig_Info] = Field(default=None, description="脚本基础信息")
Script: Optional[GeneralConfig_Script] = Field(default=None, description="脚本配置")
Game: Optional[GeneralConfig_Game] = Field(default=None, description="游戏配置")
Run: Optional[GeneralConfig_Run] = Field(default=None, description="运行配置")
class PlanIndexItem(BaseModel):
uid: str = Field(..., description="唯一标识符")
type: Literal["MaaPlanConfig"] = Field(..., description="配置类型")
class MaaPlanConfig_Info(BaseModel):
Name: Optional[str] = Field(default=None, description="计划表名称")
Mode: Optional[Literal["ALL", "Weekly"]] = Field(
default=None, description="计划表模式"
)
class MaaPlanConfig_Item(BaseModel):
MedicineNumb: Optional[int] = Field(default=None, description="吃理智药")
SeriesNumb: Optional[Literal["0", "6", "5", "4", "3", "2", "1", "-1"]] = Field(
None, description="连战次数"
)
Stage: Optional[str] = Field(default=None, description="关卡选择")
Stage_1: Optional[str] = Field(default=None, description="备选关卡 - 1")
Stage_2: Optional[str] = Field(default=None, description="备选关卡 - 2")
Stage_3: Optional[str] = Field(default=None, description="备选关卡 - 3")
Stage_Remain: Optional[str] = Field(default=None, description="剩余理智关卡")
class MaaPlanConfig(BaseModel):
Info: Optional[MaaPlanConfig_Info] = Field(default=None, description="基础信息")
ALL: Optional[MaaPlanConfig_Item] = Field(default=None, description="全局")
Monday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周一")
Tuesday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周二")
Wednesday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周三")
Thursday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周四")
Friday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周五")
Saturday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周六")
Sunday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周日")
class HistoryIndexItem(BaseModel):
date: str = Field(..., description="日期")
status: Literal["完成", "异常"] = Field(..., description="状态")
jsonFile: str = Field(..., description="对应JSON文件")
class HistoryData(BaseModel):
index: Optional[List[HistoryIndexItem]] = Field(
default=None, description="历史记录索引列表"
)
recruit_statistics: Optional[Dict[str, int]] = Field(
default=None, description="公招统计数据, key为星级, value为对应的公招数量"
)
drop_statistics: Optional[Dict[str, Dict[str, int]]] = Field(
default=None,
description="掉落统计数据, 格式为 { '关卡号': { '掉落物': 数量 } }",
)
error_info: Optional[Dict[str, str]] = Field(
default=None, description="报错信息, key为时间戳, value为错误描述"
)
log_content: Optional[str] = Field(
default=None, description="日志内容, 仅在提取单条历史记录数据时返回"
)
class ScriptCreateIn(BaseModel):
type: Literal["MAA", "General"] = Field(
..., description="脚本类型: MAA脚本, 通用脚本"
)
class ScriptCreateOut(OutBase):
scriptId: str = Field(..., description="新创建的脚本ID")
data: Union[MaaConfig, GeneralConfig] = Field(..., description="脚本配置数据")
class ScriptGetIn(BaseModel):
scriptId: Optional[str] = Field(
default=None, description="脚本ID, 未携带时表示获取所有脚本数据"
)
class ScriptGetOut(OutBase):
index: List[ScriptIndexItem] = Field(..., description="脚本索引列表")
data: Dict[str, Union[MaaConfig, GeneralConfig]] = Field(
..., description="脚本数据字典, key来自于index列表的uid"
)
class ScriptUpdateIn(BaseModel):
scriptId: str = Field(..., description="脚本ID")
data: Union[MaaConfig, GeneralConfig] = Field(..., description="脚本更新数据")
class ScriptDeleteIn(BaseModel):
scriptId: str = Field(..., description="脚本ID")
class ScriptReorderIn(BaseModel):
indexList: List[str] = Field(..., description="脚本ID列表, 按新顺序排列")
class ScriptFileIn(BaseModel):
scriptId: str = Field(..., description="脚本ID")
jsonFile: str = Field(..., description="配置文件路径")
class ScriptUrlIn(BaseModel):
scriptId: str = Field(..., description="脚本ID")
url: str = Field(..., description="配置文件URL")
class ScriptUploadIn(BaseModel):
scriptId: str = Field(..., description="脚本ID")
config_name: str = Field(..., description="配置名称")
author: str = Field(..., description="作者")
description: str = Field(..., description="描述")
class UserInBase(BaseModel):
scriptId: str = Field(..., description="所属脚本ID")
class UserGetIn(UserInBase):
userId: Optional[str] = Field(
default=None, description="用户ID, 未携带时表示获取所有用户数据"
)
class UserGetOut(OutBase):
index: List[UserIndexItem] = Field(..., description="用户索引列表")
data: Dict[str, Union[MaaUserConfig, GeneralUserConfig]] = Field(
..., description="用户数据字典, key来自于index列表的uid"
)
class UserCreateOut(OutBase):
userId: str = Field(..., description="新创建的用户ID")
data: Union[MaaUserConfig, GeneralUserConfig] = Field(
..., description="用户配置数据"
)
class UserUpdateIn(UserInBase):
userId: str = Field(..., description="用户ID")
data: Union[MaaUserConfig, GeneralUserConfig] = Field(
..., description="用户更新数据"
)
class UserDeleteIn(UserInBase):
userId: str = Field(..., description="用户ID")
class UserReorderIn(UserInBase):
indexList: List[str] = Field(..., description="用户ID列表, 按新顺序排列")
class UserSetIn(UserInBase):
userId: str = Field(..., description="用户ID")
jsonFile: str = Field(..., description="JSON文件路径, 用于导入自定义基建文件")
class WebhookInBase(BaseModel):
scriptId: Optional[str] = Field(
default=None, description="所属脚本ID, 获取全局设置的Webhook数据时无需携带"
)
userId: Optional[str] = Field(
default=None, description="所属用户ID, 获取全局设置的Webhook数据时无需携带"
)
class WebhookGetIn(WebhookInBase):
webhookId: Optional[str] = Field(
default=None, description="Webhook ID, 未携带时表示获取所有Webhook数据"
)
class WebhookGetOut(OutBase):
index: List[WebhookIndexItem] = Field(..., description="Webhook索引列表")
data: Dict[str, Webhook] = Field(
..., description="Webhook数据字典, key来自于index列表的uid"
)
class WebhookCreateOut(OutBase):
webhookId: str = Field(..., description="新创建的Webhook ID")
data: Webhook = Field(..., description="Webhook配置数据")
class WebhookUpdateIn(WebhookInBase):
webhookId: str = Field(..., description="Webhook ID")
data: Webhook = Field(..., description="Webhook更新数据")
class WebhookDeleteIn(WebhookInBase):
webhookId: str = Field(..., description="Webhook ID")
class WebhookReorderIn(WebhookInBase):
indexList: List[str] = Field(..., description="Webhook ID列表, 按新顺序排列")
class WebhookTestIn(WebhookInBase):
data: Webhook = Field(..., description="Webhook配置数据")
class PlanCreateIn(BaseModel):
type: Literal["MaaPlan"]
class PlanCreateOut(OutBase):
planId: str = Field(..., description="新创建的计划ID")
data: MaaPlanConfig = Field(..., description="计划配置数据")
class PlanGetIn(BaseModel):
planId: Optional[str] = Field(
default=None, description="计划ID, 未携带时表示获取所有计划数据"
)
class PlanGetOut(OutBase):
index: List[PlanIndexItem] = Field(..., description="计划索引列表")
data: Dict[str, MaaPlanConfig] = Field(..., description="计划列表或单个计划数据")
class PlanUpdateIn(BaseModel):
planId: str = Field(..., description="计划ID")
data: MaaPlanConfig = Field(..., description="计划更新数据")
class PlanDeleteIn(BaseModel):
planId: str = Field(..., description="计划ID")
class PlanReorderIn(BaseModel):
indexList: List[str] = Field(..., description="计划ID列表, 按新顺序排列")
class QueueCreateOut(OutBase):
queueId: str = Field(..., description="新创建的队列ID")
data: QueueConfig = Field(..., description="队列配置数据")
class QueueGetIn(BaseModel):
queueId: Optional[str] = Field(
default=None, description="队列ID, 未携带时表示获取所有队列数据"
)
class QueueGetOut(OutBase):
index: List[QueueIndexItem] = Field(..., description="队列索引列表")
data: Dict[str, QueueConfig] = Field(
..., description="队列数据字典, key来自于index列表的uid"
)
class QueueUpdateIn(BaseModel):
queueId: str = Field(..., description="队列ID")
data: QueueConfig = Field(..., description="队列更新数据")
class QueueDeleteIn(BaseModel):
queueId: str = Field(..., description="队列ID")
class QueueReorderIn(BaseModel):
indexList: List[str] = Field(..., description="按新顺序排列的调度队列UID列表")
class QueueSetInBase(BaseModel):
queueId: str = Field(..., description="所属队列ID")
class TimeSetGetIn(QueueSetInBase):
timeSetId: Optional[str] = Field(
default=None, description="时间设置ID, 未携带时表示获取所有时间设置数据"
)
class TimeSetGetOut(OutBase):
index: List[TimeSetIndexItem] = Field(..., description="时间设置索引列表")
data: Dict[str, TimeSet] = Field(
..., description="时间设置数据字典, key来自于index列表的uid"
)
class TimeSetCreateOut(OutBase):
timeSetId: str = Field(..., description="新创建的时间设置ID")
data: TimeSet = Field(..., description="时间设置配置数据")
class TimeSetUpdateIn(QueueSetInBase):
timeSetId: str = Field(..., description="时间设置ID")
data: TimeSet = Field(..., description="时间设置更新数据")
class TimeSetDeleteIn(QueueSetInBase):
timeSetId: str = Field(..., description="时间设置ID")
class TimeSetReorderIn(QueueSetInBase):
indexList: List[str] = Field(..., description="时间设置ID列表, 按新顺序排列")
class QueueItemGetIn(QueueSetInBase):
queueItemId: Optional[str] = Field(
default=None, description="队列项ID, 未携带时表示获取所有队列项数据"
)
class QueueItemGetOut(OutBase):
index: List[QueueItemIndexItem] = Field(..., description="队列项索引列表")
data: Dict[str, QueueItem] = Field(
..., description="队列项数据字典, key来自于index列表的uid"
)
class QueueItemCreateOut(OutBase):
queueItemId: str = Field(..., description="新创建的队列项ID")
data: QueueItem = Field(..., description="队列项配置数据")
class QueueItemUpdateIn(QueueSetInBase):
queueItemId: str = Field(..., description="队列项ID")
data: QueueItem = Field(..., description="队列项更新数据")
class QueueItemDeleteIn(QueueSetInBase):
queueItemId: str = Field(..., description="队列项ID")
class QueueItemReorderIn(QueueSetInBase):
indexList: List[str] = Field(..., description="队列项ID列表, 按新顺序排列")
class DispatchIn(BaseModel):
taskId: str = Field(
...,
description="目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID",
)
class TaskCreateIn(DispatchIn):
mode: Literal["自动代理", "人工排查", "设置脚本"] = Field(
..., description="任务模式"
)
class TaskCreateOut(OutBase):
websocketId: str = Field(..., description="新创建的任务ID")
class WebSocketMessage(BaseModel):
id: str = Field(..., description="消息ID, 为Main时表示消息来自主进程")
type: Literal["Update", "Message", "Info", "Signal"] = Field(
...,
description="消息类型 Update: 更新数据, Message: 请求弹出对话框, Info: 需要在UI显示的消息, Signal: 程序信号",
)
data: Dict[str, Any] = Field(..., description="消息数据, 具体内容根据type类型而定")
class PowerIn(BaseModel):
signal: Literal[
"NoAction", "Shutdown", "ShutdownForce", "Hibernate", "Sleep", "KillSelf"
] = Field(..., description="电源操作信号")
class HistorySearchIn(BaseModel):
mode: Literal["按日合并", "按周合并", "按月合并"] = Field(
..., description="合并模式"
)
start_date: str = Field(..., description="开始日期, 格式YYYY-MM-DD")
end_date: str = Field(..., description="结束日期, 格式YYYY-MM-DD")
class HistorySearchOut(OutBase):
data: Dict[str, Dict[str, HistoryData]] = Field(
...,
description="历史记录索引数据字典, 格式为 { '日期': { '用户名': [历史记录信息] } }",
)
class HistoryDataGetIn(BaseModel):
jsonPath: str = Field(..., description="需要提取数据的历史记录JSON文件")
class HistoryDataGetOut(OutBase):
data: HistoryData = Field(..., description="历史记录数据")
class SettingGetOut(OutBase):
data: GlobalConfig = Field(..., description="全局设置数据")
class SettingUpdateIn(BaseModel):
data: GlobalConfig = Field(..., description="全局设置需要更新的数据")
class UpdateCheckIn(BaseModel):
current_version: str = Field(..., description="当前前端版本号")
if_force: bool = Field(default=False, description="是否强制拉取更新信息")
class UpdateCheckOut(OutBase):
if_need_update: bool = Field(..., description="是否需要更新前端")
latest_version: str = Field(..., description="最新前端版本号")
update_info: Dict[str, List[str]] = Field(..., description="版本更新信息字典")

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

@@ -0,0 +1,31 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
__version__ = "5.0.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .matomo import Matomo
from .notification import Notify
from .system import System
from .update import Updater
__all__ = ["Matomo", "Notify", "System", "Updater"]

125
app/services/matomo.py Normal file
View File

@@ -0,0 +1,125 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import asyncio
import aiohttp
import json
import uuid
import platform
import time
from typing import Dict, Any, Optional
from app.core import Config
from app.utils.logger import get_logger
logger = get_logger("信息上报")
class _MatomoHandler:
"""Matomo统计上报服务"""
base_url = "https://statistics.auto-mas.top/matomo.php"
site_id = "3"
def __init__(self):
self.session = None
async def _get_session(self):
"""获取HTTP会话"""
if self.session is None or self.session.closed:
timeout = aiohttp.ClientTimeout(total=10)
self.session = aiohttp.ClientSession(timeout=timeout)
return self.session
async def close(self):
"""关闭HTTP会话"""
if self.session and not self.session.closed:
await self.session.close()
def _build_base_params(self, custom_vars: Optional[Dict[str, Any]] = None):
"""构建基础参数"""
params = {
"idsite": self.site_id,
"rec": "1",
"action_name": "AUTO-MAS后端",
"_id": Config.get("Data", "UID")[:16],
"uid": Config.get("Data", "UID"),
"rand": str(uuid.uuid4().int)[:10],
"apiv": "1",
"h": time.strftime("%H"),
"m": time.strftime("%M"),
"s": time.strftime("%S"),
"ua": f"AUTO-MAS/{Config.version()} ({platform.system()} {platform.release()})",
}
# 添加自定义变量
if custom_vars is not None:
cvar = {}
for i, (key, value) in enumerate(custom_vars.items(), 1):
if i <= 5:
cvar[str(i)] = [str(key), str(value)]
if cvar:
params["_cvar"] = json.dumps(cvar)
return params
async def send_event(
self,
category: str,
action: str,
name: Optional[str] = None,
value: Optional[float] = None,
custom_vars: Optional[Dict[str, Any]] = None,
):
"""发送事件数据到Matomo
Args:
category: 事件类别,如 "Script", "Config", "User"
action: 事件动作,如 "Execute", "Update", "Login"
name: 事件名称,如具体的脚本名称
value: 事件值,如执行时长、文件大小等数值
custom_vars: 自定义变量字典
"""
try:
session = await self._get_session()
if session is None:
return
params = self._build_base_params(custom_vars)
params.update({"e_c": category, "e_a": action, "e_n": name, "e_v": value})
params = {k: v for k, v in params.items() if v is not None}
async with session.get(self.base_url, params=params) as response:
if response.status == 200:
logger.debug(f"Matomo事件上报成功: {category}/{action}")
else:
logger.warning(f"Matomo事件上报失败: {response.status}")
except asyncio.TimeoutError:
logger.warning("Matomo事件上报超时")
except Exception as e:
logger.error(f"Matomo事件上报错误: {e}")
Matomo = _MatomoHandler()

View File

@@ -0,0 +1,431 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import re
import json
import smtplib
import requests
from datetime import datetime
from plyer import notification
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formataddr
from pathlib import Path
from typing import Literal
from app.core import Config
from app.models.config import Webhook
from app.utils import get_logger, ImageUtils
logger = get_logger("通知服务")
class Notification:
async def push_plyer(self, title: str, message: str, ticker: str, t: int) -> None:
"""
推送系统通知
Parameters
----------
title: str
通知标题
message: str
通知内容
ticker: str
通知横幅
t: int
通知持续时间
"""
if not Config.get("Notify", "IfPushPlyer"):
return
logger.info(f"推送系统通知: {title}")
if notification.notify is not None:
notification.notify(
title=title,
message=message,
app_name="AUTO-MAS",
app_icon=(Path.cwd() / "res/icons/AUTO-MAS.ico").as_posix(),
timeout=t,
ticker=ticker,
toast=True,
)
else:
logger.error("plyer.notification 未正确导入, 无法推送系统通知")
async def send_mail(
self, mode: Literal["文本", "网页"], title: str, content: str, to_address: str
) -> None:
"""
推送邮件通知
Parameters
----------
mode: Literal["文本", "网页"]
邮件内容模式, 支持 "文本""网页"
title: str
邮件标题
content: str
邮件内容
to_address: str
收件人地址
"""
if Config.get("Notify", "SMTPServerAddress") == "":
raise ValueError("邮件通知的SMTP服务器地址不能为空")
if Config.get("Notify", "AuthorizationCode") == "":
raise ValueError("邮件通知的授权码不能为空")
if not bool(
re.match(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
Config.get("Notify", "FromAddress"),
)
):
raise ValueError("邮件通知的发送邮箱格式错误或为空")
if not bool(
re.match(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
to_address,
)
):
raise ValueError("邮件通知的接收邮箱格式错误或为空")
# 定义邮件正文
if mode == "文本":
message = MIMEText(content, "plain", "utf-8")
elif mode == "网页":
message = MIMEMultipart("alternative")
message["From"] = formataddr(
(
Header("AUTO-MAS通知服务", "utf-8").encode(),
Config.get("Notify", "FromAddress"),
)
) # 发件人显示的名字
message["To"] = formataddr(
(Header("AUTO-MAS用户", "utf-8").encode(), to_address)
) # 收件人显示的名字
message["Subject"] = str(Header(title, "utf-8"))
if mode == "网页":
message.attach(MIMEText(content, "html", "utf-8"))
smtpObj = smtplib.SMTP_SSL(Config.get("Notify", "SMTPServerAddress"), 465)
smtpObj.login(
Config.get("Notify", "FromAddress"),
Config.get("Notify", "AuthorizationCode"),
)
smtpObj.sendmail(
Config.get("Notify", "FromAddress"), to_address, message.as_string()
)
smtpObj.quit()
logger.success(f"邮件发送成功: {title}")
async def ServerChanPush(self, title: str, content: str, send_key: str) -> None:
"""
使用Server酱推送通知
Parameters
----------
title: str
通知标题
content: str
通知内容
send_key: str
Server酱的SendKey
"""
if send_key == "":
raise ValueError("ServerChan SendKey 不能为空")
# 构造 URL
if send_key.startswith("sctp"):
match = re.match(r"^sctp(\d+)t", send_key)
if match:
url = f"https://{match.group(1)}.push.ft07.com/send/{send_key}.send"
else:
raise ValueError("SendKey 格式不正确 (sctp<int>)")
else:
url = f"https://sctapi.ftqq.com/{send_key}.send"
# 请求发送
params = {"title": title, "desp": content}
headers = {"Content-Type": "application/json;charset=utf-8"}
response = requests.post(
url, json=params, headers=headers, timeout=10, proxies=Config.get_proxies()
)
result = response.json()
if result.get("code") == 0:
logger.success(f"Server酱推送通知成功: {title}")
else:
raise Exception(f"ServerChan 推送通知失败: {response.text}")
async def WebhookPush(self, title: str, content: str, webhook: Webhook) -> None:
"""
Webhook 推送通知
Parameters
----------
title: str
通知标题
content: str
通知内容
webhook: Webhook
Webhook配置对象
"""
if not webhook.get("Info", "Enabled"):
return
if webhook.get("Data", "Url") == "":
raise ValueError("Webhook URL 不能为空")
# 解析模板
template = (
webhook.get("Data", "Template")
or '{"title": "{title}", "content": "{content}"}'
)
# 替换模板变量
try:
# 准备模板变量
template_vars = {
"title": title,
"content": content,
"datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"date": datetime.now().strftime("%Y-%m-%d"),
"time": datetime.now().strftime("%H:%M:%S"),
}
logger.debug(f"原始模板: {template}")
logger.debug(f"模板变量: {template_vars}")
# 先尝试作为JSON模板处理
try:
# 解析模板为JSON对象然后替换其中的变量
template_obj = json.loads(template)
# 递归替换JSON对象中的变量
def replace_variables(obj):
if isinstance(obj, dict):
return {k: replace_variables(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [replace_variables(item) for item in obj]
elif isinstance(obj, str):
result = obj
for key, value in template_vars.items():
result = result.replace(f"{{{key}}}", str(value))
return result
else:
return obj
data = replace_variables(template_obj)
logger.debug(f"成功解析JSON模板: {data}")
except json.JSONDecodeError:
# 如果不是有效的JSON作为字符串模板处理
logger.debug("模板不是有效JSON作为字符串模板处理")
formatted_template = template
for key, value in template_vars.items():
# 转义特殊字符以避免JSON解析错误
safe_value = (
str(value)
.replace('"', '\\"')
.replace("\n", "\\n")
.replace("\r", "\\r")
)
formatted_template = formatted_template.replace(
f"{{{key}}}", safe_value
)
# 再次尝试解析为JSON
try:
data = json.loads(formatted_template)
logger.debug(f"字符串模板解析为JSON成功: {data}")
except json.JSONDecodeError:
# 最终作为纯文本发送
data = formatted_template
logger.debug(f"作为纯文本发送: {data}")
except Exception as e:
logger.warning(f"模板解析失败,使用默认格式: {e}")
data = {"title": title, "content": content}
# 准备请求头
headers = {"Content-Type": "application/json"}
headers.update(json.loads(webhook.get("Data", "Headers")))
if webhook.get("Data", "Method") == "POST":
if isinstance(data, dict):
response = requests.post(
url=webhook.get("Data", "Url"),
json=data,
headers=headers,
timeout=10,
proxies=Config.get_proxies(),
)
elif isinstance(data, str):
response = requests.post(
url=webhook.get("Data", "Url"),
data=data,
headers=headers,
timeout=10,
proxies=Config.get_proxies(),
)
elif webhook.get("Data", "Method") == "GET":
if isinstance(data, dict):
# Flatten params to ensure all values are str or list of str
params = {}
for k, v in data.items():
if isinstance(v, (dict, list)):
params[k] = json.dumps(v, ensure_ascii=False)
else:
params[k] = str(v)
else:
params = {"message": str(data)}
response = requests.get(
url=webhook.get("Data", "Url"),
params=params,
headers=headers,
timeout=10,
proxies=Config.get_proxies(),
)
# 检查响应
if response.status_code == 200:
logger.success(
f"自定义Webhook推送成功: {webhook.get('Info', 'Name')} - {title}"
)
else:
raise Exception(f"HTTP {response.status_code}: {response.text}")
async def _WebHookPush(self, title, content, webhook_url) -> None:
"""
WebHook 推送通知 (即将弃用)
:param title: 通知标题
:param content: 通知内容
:param webhook_url: WebHook地址
"""
if not webhook_url:
raise ValueError("WebHook 地址不能为空")
content = f"{title}\n{content}"
data = {"msgtype": "text", "text": {"content": content}}
response = requests.post(
url=webhook_url, json=data, timeout=10, proxies=Config.get_proxies()
)
info = response.json()
if info["errcode"] == 0:
logger.success(f"WebHook 推送通知成功: {title}")
else:
raise Exception(f"WebHook 推送通知失败: {response.text}")
async def CompanyWebHookBotPushImage(
self, image_path: Path, webhook_url: str
) -> None:
"""
使用企业微信群机器人推送图片通知(等待重新适配)
:param image_path: 图片文件路径
:param webhook_url: 企业微信群机器人的WebHook地址
"""
if not webhook_url:
raise ValueError("webhook URL 不能为空")
# 压缩图片
ImageUtils.compress_image_if_needed(image_path)
# 检查图片是否存在
if not image_path.exists():
raise FileNotFoundError(f"文件未找到: {image_path}")
# 获取图片base64和md5
image_base64 = ImageUtils.get_base64_from_file(str(image_path))
image_md5 = ImageUtils.calculate_md5_from_file(str(image_path))
data = {
"msgtype": "image",
"image": {"base64": image_base64, "md5": image_md5},
}
response = requests.post(
url=webhook_url, json=data, timeout=10, proxies=Config.get_proxies()
)
info = response.json()
if info.get("errcode") == 0:
logger.success(f"企业微信群机器人推送图片成功: {image_path.name}")
else:
raise Exception(f"企业微信群机器人推送图片失败: {response.text}")
async def send_test_notification(self) -> None:
"""发送测试通知到所有已启用的通知渠道"""
logger.info("发送测试通知到所有已启用的通知渠道")
# 发送系统通知
await self.push_plyer(
"测试通知",
"这是 AUTO-MAS 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
"测试通知",
3,
)
# 发送邮件通知
if Config.get("Notify", "IfSendMail"):
await self.send_mail(
"文本",
"AUTO-MAS测试通知",
"这是 AUTO-MAS 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
Config.get("Notify", "ToAddress"),
)
# 发送Server酱通知
if Config.get("Notify", "IfServerChan"):
await self.ServerChanPush(
"AUTO-MAS测试通知",
"这是 AUTO-MAS 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
Config.get("Notify", "ServerChanKey"),
)
# 发送自定义Webhook通知
for webhook in Config.Notify_CustomWebhooks.values():
await self.WebhookPush(
"AUTO-MAS测试通知",
"这是 AUTO-MAS 外部通知测试信息。如果你看到了这段内容, 说明 AUTO-MAS 的通知功能已经正确配置且可以正常工作!",
webhook,
)
logger.success("测试通知发送完成")
Notify = Notification()

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

@@ -0,0 +1,383 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import sys
import ctypes
import asyncio
import win32gui
import win32process
import psutil
import subprocess
import tempfile
import getpass
from datetime import datetime
from pathlib import Path
from typing import Literal, Optional
from app.core import Config
from app.models.schema import WebSocketMessage
from app.utils.logger import get_logger
logger = get_logger("系统服务")
class _SystemHandler:
ES_CONTINUOUS = 0x80000000
ES_SYSTEM_REQUIRED = 0x00000001
countdown = 60
def __init__(self) -> None:
self.power_task: Optional[asyncio.Task] = None
async def set_Sleep(self) -> None:
"""同步系统休眠状态"""
if Config.get("Function", "IfAllowSleep"):
# 设置系统电源状态
ctypes.windll.kernel32.SetThreadExecutionState(
self.ES_CONTINUOUS | self.ES_SYSTEM_REQUIRED
)
else:
# 恢复系统电源状态
ctypes.windll.kernel32.SetThreadExecutionState(self.ES_CONTINUOUS)
async def set_SelfStart(self) -> None:
"""同步开机自启"""
if Config.get("Start", "IfSelfStart") and not await self.is_startup():
# 创建任务计划
try:
# 获取当前用户和时间
current_user = getpass.getuser()
current_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
# XML 模板
xml_content = f"""<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Date>{current_time}</Date>
<Author>{current_user}</Author>
<Description>AUTO-MAS自启动服务</Description>
<URI>\\AUTO-MAS_AutoStart</URI>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<StartBoundary>{current_time}</StartBoundary>
<Enabled>true</Enabled>
</LogonTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<LogonType>InteractiveToken</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>false</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>"{Path.cwd() / 'AUTO-MAS.exe'}"</Command>
</Exec>
</Actions>
</Task>"""
# 创建临时 XML 文件并执行
with tempfile.NamedTemporaryFile(
mode="w", suffix=".xml", delete=False, encoding="utf-16"
) as f:
f.write(xml_content)
xml_file = f.name
try:
result = subprocess.run(
[
"schtasks",
"/create",
"/tn",
"AUTO-MAS_AutoStart",
"/xml",
xml_file,
"/f",
],
creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
)
if result.returncode == 0:
logger.success(
f"程序自启动任务计划已创建: {Path.cwd() / 'AUTO-MAS.exe'}"
)
else:
logger.error(f"程序自启动任务计划创建失败: {result.stderr}")
finally:
# 删除临时文件
try:
Path(xml_file).unlink()
except:
pass
except Exception as e:
logger.exception(f"程序自启动任务计划创建失败: {e}")
elif not Config.get("Start", "IfSelfStart") and await self.is_startup():
try:
result = subprocess.run(
["schtasks", "/delete", "/tn", "AUTO-MAS_AutoStart", "/f"],
creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
)
if result.returncode == 0:
logger.success("程序自启动任务计划已删除")
else:
logger.error(f"程序自启动任务计划删除失败: {result.stderr}")
except Exception as e:
logger.exception(f"程序自启动任务计划删除失败: {e}")
async def set_power(
self,
mode: Literal[
"NoAction", "Shutdown", "ShutdownForce", "Hibernate", "Sleep", "KillSelf"
],
) -> None:
"""
执行系统电源操作
:param mode: 电源操作
"""
if sys.platform.startswith("win"):
if mode == "NoAction":
logger.info("不执行系统电源操作")
elif mode == "Shutdown":
await self.kill_emulator_processes()
logger.info("执行关机操作")
subprocess.run(["shutdown", "/s", "/t", "0"])
elif mode == "ShutdownForce":
logger.info("执行强制关机操作")
subprocess.run(["shutdown", "/s", "/t", "0", "/f"])
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" and Config.server is not None:
logger.info("执行退出主程序操作")
Config.server.should_exit = True
elif sys.platform.startswith("linux"):
if mode == "NoAction":
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" and Config.server is not None:
logger.info("执行退出主程序操作")
Config.server.should_exit = True
async def _power_task(
self,
power_sign: Literal[
"NoAction", "Shutdown", "ShutdownForce", "Hibernate", "Sleep", "KillSelf"
],
) -> None:
"""电源任务"""
await asyncio.sleep(self.countdown)
if power_sign == "KillSelf":
await Config.send_json(
WebSocketMessage(
id="Main", type="Signal", data={"RequestClose": "请求前端关闭"}
).model_dump()
)
await self.set_power(power_sign)
async def start_power_task(self):
"""开始电源任务"""
if self.power_task is None or self.power_task.done():
self.power_task = asyncio.create_task(self._power_task(Config.power_sign))
logger.info(
f"电源任务已启动, {self.countdown}秒后执行: {Config.power_sign}"
)
else:
logger.warning("已有电源任务在运行, 请勿重复启动")
async def cancel_power_task(self):
"""取消电源任务"""
if self.power_task is not None and not self.power_task.done():
self.power_task.cancel()
try:
await self.power_task
except asyncio.CancelledError:
logger.info("电源任务已取消")
else:
logger.warning("当前无电源任务在运行")
raise RuntimeError("当前无电源任务在运行")
async def kill_emulator_processes(self):
"""这里暂时仅支持 MuMu 模拟器"""
logger.info("正在清除模拟器进程")
keywords = ["Nemu", "nemu", "emulator", "MuMu"]
for proc in psutil.process_iter(["pid", "name"]):
try:
pname = proc.info["name"].lower()
if any(keyword.lower() in pname for keyword in keywords):
proc.kill()
logger.info(f"已关闭 MuMu 模拟器进程: {proc.info['name']}")
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
logger.success("模拟器进程清除完成")
async def is_startup(self) -> bool:
"""判断程序是否已经开机自启"""
try:
result = subprocess.run(
["schtasks", "/query", "/tn", "AUTO-MAS_AutoStart"],
creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
)
return result.returncode == 0
except Exception as e:
logger.exception(f"检查任务计划程序失败: {e}")
return False
async 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
async def kill_process(self, path: Path) -> None:
"""
根据路径中止进程
:param path: 进程路径
"""
logger.info(f"开始中止进程: {path}")
for pid in await self.search_pids(path):
killprocess = subprocess.Popen(
f"taskkill /F /T /PID {pid}",
shell=True,
creationflags=subprocess.CREATE_NO_WINDOW,
)
killprocess.wait()
logger.success(f"进程已中止: {path}")
async def search_pids(self, path: Path) -> list:
"""
根据路径查找进程PID
:param path: 进程路径
:return: 匹配的进程PID列表
"""
logger.info(f"开始查找进程 PID: {path}")
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()

389
app/services/update.py Normal file
View File

@@ -0,0 +1,389 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import re
import time
import json
import asyncio
import zipfile
import requests
import subprocess
from packaging import version
from datetime import datetime, timedelta
from typing import List, Dict, Optional
from pathlib import Path
from app.core import Config
from app.models.schema import WebSocketMessage
from app.utils.constants import MIRROR_ERROR_INFO
from app.utils.logger import get_logger
logger = get_logger("更新服务")
class _UpdateHandler:
def __init__(self) -> None:
self.is_locked: bool = False
self.remote_version: Optional[str] = None
self.last_check_time: Optional[datetime] = None
self.update_version_info: Optional[Dict[str, List[str]]] = None
self.mirror_chyan_download_url: Optional[str] = None
async def check_update(
self, current_version: str, if_force: bool = False
) -> tuple[bool, str, Dict[str, List[str]]]:
if (
not if_force
and self.remote_version is not None
and self.last_check_time is not None
and self.update_version_info is not None
and self.last_check_time > datetime.now() - timedelta(hours=4)
):
logger.info("四小时内已进行过一次检查, 直接使用缓存的版本更新信息")
return (
bool(
version.parse(self.remote_version) > version.parse(current_version)
),
self.remote_version,
self.update_version_info,
)
logger.info("开始检查更新")
response = requests.get(
f"https://mirrorchyan.com/api/resources/AUTO_MAA/latest?user_agent=AutoMasGui&current_version={current_version}&cdk={Config.get('Update', 'MirrorChyanCDK')}&channel=stable",
timeout=10,
proxies=Config.get_proxies(),
)
if response.status_code == 200:
version_info = response.json()
else:
result = response.json()
if result["code"] != 0:
if result["code"] in MIRROR_ERROR_INFO:
raise Exception(
f"获取版本信息时出错: {MIRROR_ERROR_INFO[result['code']]}"
)
else:
raise Exception(
"获取版本信息时出错: 意料之外的错误, 请及时联系项目组以获取来自 Mirror 酱的技术支持"
)
logger.success("获取版本信息成功")
self.last_check_time = datetime.now()
self.remote_version = version_info["data"]["version_name"]
if self.remote_version is None:
raise Exception("Mirror 酱未返回版本号, 请稍后重试")
if "url" in version_info["data"]:
self.mirror_chyan_download_url = version_info["data"]["url"]
if version.parse(self.remote_version) > version.parse(current_version):
# 版本更新信息
version_info_json: Dict[str, Dict[str, List[str]]] = json.loads(
re.sub(
r"^<!--\s*(.*?)\s*-->$",
r"\1",
version_info["data"]["release_note"].splitlines()[0],
)
)
self.update_version_info = {}
for v_i in [
info
for ver, info in version_info_json.items()
if version.parse(ver) > version.parse(current_version)
]:
for key, value in v_i.items():
if key not in self.update_version_info:
self.update_version_info[key] = []
self.update_version_info[key] += value
return True, self.remote_version, self.update_version_info
else:
return False, current_version, {}
async def download_update(self) -> None:
if self.is_locked:
await Config.send_json(
WebSocketMessage(
id="Update",
type="Signal",
data={"Failed": "已有更新任务在进行中, 请勿重复操作"},
).model_dump()
)
return None
self.is_locked = True
if self.remote_version is None:
await Config.send_json(
WebSocketMessage(
id="Update",
type="Signal",
data={"Failed": "未检测到可用的远程版本, 请先检查更新"},
).model_dump()
)
self.is_locked = False
return None
if (Path.cwd() / f"UpdatePack_{self.remote_version}.zip").exists():
logger.info(
f"更新包已存在: {Path.cwd() / f'UpdatePack_{self.remote_version}.zip'}"
)
await Config.send_json(
WebSocketMessage(
id="Update",
type="Signal",
data={
"Accomplish": str(
Path.cwd() / f"UpdatePack_{self.remote_version}.zip"
)
},
).model_dump()
)
self.is_locked = False
return None
if Config.get("Update", "Source") == "GitHub":
download_url = f"https://github.com/AUTO-MAS-Project/AUTO-MAS/releases/download/{self.remote_version}/AUTO-MAS_{self.remote_version}.zip"
elif Config.get("Update", "Source") == "MirrorChyan":
if self.mirror_chyan_download_url is None:
logger.warning("MirrorChyan 未返回下载链接, 使用自建下载站")
download_url = f"https://download.auto-mas.top/d/AUTO-MAS/AUTO-MAS_{self.remote_version}.zip"
else:
with requests.get(
self.mirror_chyan_download_url,
allow_redirects=True,
timeout=10,
stream=True,
proxies=Config.get_proxies(),
) as response:
if response.status_code == 200:
download_url = response.url
elif Config.get("Update", "Source") == "AutoSite":
download_url = f"https://download.auto-mas.top/d/AUTO-MAS/AUTO-MAS_{self.remote_version}.zip"
else:
await Config.send_json(
WebSocketMessage(
id="Update",
type="Signal",
data={
"Failed": f"未知的下载源: {Config.get('Update', 'Source')}, 请检查配置文件"
},
).model_dump()
)
self.is_locked = False
return None
logger.info(f"开始下载: {download_url}")
check_times = 3
while check_times != 0:
try:
# 清理可能存在的临时文件
if (Path.cwd() / "download.temp").exists():
(Path.cwd() / "download.temp").unlink()
start_time = time.time()
response = requests.get(
download_url, timeout=10, stream=True, proxies=Config.get_proxies()
)
if response.status_code not in [200, 206]:
if check_times != -1:
check_times -= 1
logger.warning(
f"连接失败: {download_url}, 状态码: {response.status_code}, 剩余重试次数: {check_times}"
)
await asyncio.sleep(1)
continue
logger.info(f"连接成功: {download_url}, 状态码: {response.status_code}")
file_size = int(response.headers.get("content-length", 0))
downloaded_size = 0
last_download_size = 0
speed = 0
last_time = time.time()
with (Path.cwd() / "download.temp").open(mode="wb") as f:
for chunk in response.iter_content(chunk_size=8192):
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()
await Config.send_json(
WebSocketMessage(
id="Update",
type="Update",
data={
"downloaded_size": downloaded_size,
"file_size": file_size,
"speed": speed,
},
).model_dump()
)
(Path.cwd() / "download.temp").rename(
Path.cwd() / f"UpdatePack_{self.remote_version}.zip"
)
logger.success(
f"下载完成: {download_url}, 实际下载大小: {downloaded_size} 字节, 耗时: {time.time() - start_time:.2f} 秒, 保存位置: {Path.cwd() / f'UpdatePack_{self.remote_version}.zip'}"
)
await Config.send_json(
WebSocketMessage(
id="Update",
type="Signal",
data={
"Accomplish": str(
Path.cwd() / f"UpdatePack_{self.remote_version}.zip"
)
},
).model_dump()
)
self.is_locked = False
break
except Exception as e:
if check_times != -1:
check_times -= 1
logger.info(
f"下载出错: {download_url}, 错误信息: {e}, 剩余重试次数: {check_times}"
)
await asyncio.sleep(1)
else:
if (Path.cwd() / "download.temp").exists():
(Path.cwd() / "download.temp").unlink()
await Config.send_json(
WebSocketMessage(
id="Update",
type="Signal",
data={"Failed": f"下载失败: {download_url}"},
).model_dump()
)
self.is_locked = False
async def install_update(self):
if self.is_locked:
await Config.send_json(
WebSocketMessage(
id="Update",
type="Signal",
data={"Failed": "已有更新任务在进行中, 请勿重复操作"},
).model_dump()
)
return None
logger.info("开始应用更新")
self.is_locked = True
versions = {
version.parse(match.group(1)): f.name
for f in Path.cwd().glob("UpdatePack_*.zip")
if (match := re.match(r"UpdatePack_(.+)\.zip$", f.name))
}
logger.info(f"检测到的更新包: {versions.values()}")
if not versions:
await Config.send_json(
WebSocketMessage(
id="Update",
type="Signal",
data={"Failed": "未检测到更新包, 请先下载更新"},
).model_dump()
)
self.is_locked = False
return None
update_package = Path.cwd() / versions[max(versions)]
logger.info(f"开始解压: {update_package}{Path.cwd()}")
try:
with zipfile.ZipFile(update_package, "r") as zip_ref:
zip_ref.extractall(Path.cwd())
except Exception as e:
logger.error(f"解压失败, {type(e).__name__}: {e}")
await Config.send_json(
WebSocketMessage(
id="Update",
type="Info",
data={"Error": f"解压失败, {type(e).__name__}: {e}"},
).model_dump()
)
self.is_locked = False
return None
logger.success(f"解压完成: {update_package}{Path.cwd()}")
logger.info("正在删除临时文件与旧更新包文件")
if (Path.cwd() / "changes.json").exists():
(Path.cwd() / "changes.json").unlink()
for f in versions.values():
if (Path.cwd() / f).exists():
(Path.cwd() / f).unlink()
logger.info("启动更新程序")
self.is_locked = False
subprocess.Popen(
[Path.cwd() / "AUTO-MAS-Setup.exe"],
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
| subprocess.DETACHED_PROCESS
| subprocess.CREATE_NO_WINDOW,
)
Updater = _UpdateHandler()

2086
app/task/MAA.py Normal file

File diff suppressed because it is too large Load Diff

32
app/task/__init__.py Normal file
View File

@@ -0,0 +1,32 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
__version__ = "5.0.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .skland import skland_sign_in
from .general import GeneralManager
from .MAA import MaaManager
__all__ = ["skland_sign_in", "GeneralManager", "MaaManager"]

1103
app/task/general.py Normal file

File diff suppressed because it is too large Load Diff

266
app/task/skland.py Normal file
View File

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

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

@@ -0,0 +1,88 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2025 ClozyA
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import base64
import hashlib
from pathlib import Path
from PIL import Image
class ImageUtils:
@staticmethod
def get_base64_from_file(image_path):
"""从本地文件读取并返回base64编码字符串"""
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
@staticmethod
def calculate_md5_from_file(image_path):
"""从本地文件读取并返回md5值hex字符串"""
with open(image_path, "rb") as f:
return hashlib.md5(f.read()).hexdigest()
@staticmethod
def calculate_md5_from_base64(base64_content):
"""从base64字符串计算md5"""
image_data = base64.b64decode(base64_content)
return hashlib.md5(image_data).hexdigest()
@staticmethod
def compress_image_if_needed(image_path: Path, max_size_mb=2) -> Path:
"""
如果图片大于max_size_mb, 则压缩并覆盖原文件, 返回原始路径Path对象
"""
RESAMPLE = Image.Resampling.LANCZOS # Pillow 9.1.0及以后
max_size = max_size_mb * 1024 * 1024
if image_path.stat().st_size <= max_size:
return image_path
img = Image.open(image_path)
suffix = image_path.suffix.lower()
quality = 90 if suffix in [".jpg", ".jpeg"] else None
step = 5
if quality is not None:
while True:
img.save(image_path, quality=quality, optimize=True)
if image_path.stat().st_size <= max_size or quality <= 10:
break
quality -= step
elif suffix == ".png":
width, height = img.size
while True:
img.save(image_path, optimize=True)
if (
image_path.stat().st_size <= max_size
or width <= 200
or height <= 200
):
break
width = int(width * 0.95)
height = int(height * 0.95)
img = img.resize((width, height), RESAMPLE)
else:
raise ValueError("仅支持JPG/JPEG和PNG格式图片的压缩")
return image_path

156
app/utils/LogMonitor.py Normal file
View File

@@ -0,0 +1,156 @@
import asyncio
import aiofiles
import os
from datetime import datetime, timedelta
from pathlib import Path
from typing import Callable, Optional, List, Awaitable
from .logger import get_logger
logger = get_logger("日志监控器")
TIME_FIELDS = {
"%Y": "year",
"%m": "month",
"%d": "day",
"%H": "hour",
"%M": "minute",
"%S": "second",
"%f": "microsecond",
}
"""时间字段映射表"""
def strptime(date_string: str, format: str, default_date: datetime) -> datetime:
"""根据指定格式解析日期字符串"""
date = datetime.strptime(date_string, format)
# 构建参数字典
datetime_kwargs = {}
for format_code, field_name in TIME_FIELDS.items():
if format_code in format:
datetime_kwargs[field_name] = getattr(date, field_name)
else:
datetime_kwargs[field_name] = getattr(default_date, field_name)
return datetime(**datetime_kwargs)
class LogMonitor:
def __init__(
self,
time_stamp_range: tuple[int, int],
time_format: str,
callback: Callable[[List[str]], Awaitable[None]],
encoding: str = "utf-8",
):
self.time_stamp_range = time_stamp_range
self.time_format = time_format
self.callback = callback
self.encoding = encoding
self.log_file_path: Optional[Path] = None
self.log_start_time: datetime = datetime.now()
self.last_callback_time: datetime = datetime.now()
self.log_contents: List[str] = []
self.task: Optional[asyncio.Task] = None
self.__is_running = False
async def monitor_log(self):
"""监控日志文件的主循环"""
if self.log_file_path is None or not self.log_file_path.exists():
raise ValueError("日志文件路径未设置或文件不存在")
logger.info(f"开始监控日志文件: {self.log_file_path}")
while self.__is_running:
logger.debug("正在检查日志文件...")
log_contents = []
if_log_start = False
# 检查文件是否仍然存在
if not self.log_file_path.exists():
logger.warning(f"日志文件不存在: {self.log_file_path}")
continue
# 尝试读取文件
try:
async with aiofiles.open(
self.log_file_path, "r", encoding=self.encoding
) as f:
async for line in f:
if not if_log_start:
try:
entry_time = strptime(
line[
self.time_stamp_range[
0
] : self.time_stamp_range[1]
],
self.time_format,
self.last_callback_time,
)
if entry_time > self.log_start_time:
if_log_start = True
log_contents.append(line)
except (ValueError, IndexError):
continue
else:
log_contents.append(line)
except (FileNotFoundError, PermissionError) as e:
logger.warning(f"文件访问错误: {e}")
await asyncio.sleep(5)
continue
except UnicodeDecodeError as e:
logger.error(f"文件编码错误: {e}")
await asyncio.sleep(10)
continue
# 调用回调
if (
log_contents != self.log_contents
or datetime.now() - self.last_callback_time > timedelta(minutes=1)
):
self.log_contents = log_contents
self.last_callback_time = datetime.now()
# 安全调用回调函数
try:
await self.callback(log_contents)
except Exception as e:
logger.error(f"回调函数执行失败: {e}")
await asyncio.sleep(1)
async def start(self, log_file_path: Path, start_time: datetime) -> None:
"""启动监控"""
if log_file_path.is_dir():
raise ValueError(f"日志文件不能是目录: {log_file_path}")
if self.task is not None and not self.task.done():
await self.stop()
self.__is_running = True
self.log_contents = []
self.log_file_path = log_file_path
self.log_start_time = start_time
self.task = asyncio.create_task(self.monitor_log())
logger.info(f"日志监控已启动: {self.log_file_path}")
async def stop(self):
"""停止监控"""
logger.info("请求取消日志监控任务")
if self.task is not None and not self.task.done():
self.task.cancel()
try:
await self.task
except asyncio.CancelledError:
logger.info("日志监控任务已中止")
logger.success("日志监控任务已停止")
self.task = None

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

@@ -0,0 +1,156 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import asyncio
import psutil
import subprocess
from datetime import datetime, timedelta
from pathlib import Path
class ProcessManager:
"""进程监视器类, 用于跟踪主进程及其所有子进程的状态"""
def __init__(self):
super().__init__()
self.main_pid = None
self.tracked_pids = set()
self.check_task = None
self.track_end_time = datetime.now()
async def open_process(
self, path: Path, args: list = [], tracking_time: int = 60
) -> None:
"""
启动一个新进程并返回其pid, 并开始监视该进程
Parameters
----------
path: 可执行文件的路径
args: 启动参数列表
tracking_time: 子进程追踪持续时间(秒)
"""
process = subprocess.Popen(
[path, *args],
cwd=path.parent,
creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
await self.start_monitoring(process.pid, tracking_time)
async def start_monitoring(self, pid: int, tracking_time: int = 60) -> None:
"""
启动进程监视器, 跟踪指定的主进程及其子进程
:param pid: 被监视进程的PID
:param tracking_time: 子进程追踪持续时间(秒)
"""
await self.clear()
self.main_pid = pid
self.tracking_time = tracking_time
# 扫描并记录所有相关进程
try:
# 获取主进程
main_proc = psutil.Process(self.main_pid)
self.tracked_pids.add(self.main_pid)
# 递归获取所有子进程
if tracking_time:
for child in main_proc.children(recursive=True):
self.tracked_pids.add(child.pid)
except psutil.NoSuchProcess:
pass
# 启动持续追踪任务
if tracking_time > 0:
self.track_end_time = datetime.now() + timedelta(seconds=tracking_time)
self.check_task = asyncio.create_task(self.track_processes())
async def track_processes(self) -> None:
"""更新子进程列表"""
while datetime.now() < self.track_end_time:
current_pids = set(self.tracked_pids)
for pid in current_pids:
try:
proc = psutil.Process(pid)
for child in proc.children():
if child.pid not in self.tracked_pids:
# 新发现的子进程
self.tracked_pids.add(child.pid)
except psutil.NoSuchProcess:
continue
await asyncio.sleep(0.1)
async def is_running(self) -> bool:
"""检查所有跟踪的进程是否还在运行"""
for pid in self.tracked_pids:
try:
proc = psutil.Process(pid)
if proc.is_running():
return True
except psutil.NoSuchProcess:
continue
return False
async def kill(self, if_force: bool = False) -> None:
"""停止监视器并中止所有跟踪的进程"""
for pid in self.tracked_pids:
try:
proc = psutil.Process(pid)
if if_force:
kill_process = subprocess.Popen(
["taskkill", "/F", "/T", "/PID", str(pid)],
creationflags=subprocess.CREATE_NO_WINDOW,
)
kill_process.wait()
proc.terminate()
except psutil.NoSuchProcess:
continue
await self.clear()
async def clear(self) -> None:
"""清空跟踪的进程列表"""
if self.check_task is not None and not self.check_task.done():
self.check_task.cancel()
try:
await self.check_task
except asyncio.CancelledError:
pass
self.main_pid = None
self.tracked_pids.clear()

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

@@ -0,0 +1,44 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
__version__ = "5.0.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .constants import *
from .logger import get_logger
from .ImageUtils import ImageUtils
from .LogMonitor import LogMonitor, strptime
from .ProcessManager import ProcessManager
from .security import dpapi_encrypt, dpapi_decrypt
__all__ = [
"constants",
"get_logger",
"ImageUtils",
"LogMonitor",
"ProcessManager",
"dpapi_encrypt",
"dpapi_decrypt",
"strptime",
]

292
app/utils/constants.py Normal file
View File

@@ -0,0 +1,292 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 ClozyA
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
from datetime import datetime
TYPE_BOOK = {"MaaConfig": "MAA", "GeneralConfig": "通用"}
"""配置类型映射表"""
RESOURCE_STAGE_INFO = [
{"value": "-", "text": "当前/上次", "days": [1, 2, 3, 4, 5, 6, 7]},
{"value": "1-7", "text": "1-7", "days": [1, 2, 3, 4, 5, 6, 7]},
{"value": "R8-11", "text": "R8-11", "days": [1, 2, 3, 4, 5, 6, 7]},
{"value": "12-17-HARD", "text": "12-17-HARD", "days": [1, 2, 3, 4, 5, 6, 7]},
{"value": "LS-6", "text": "经验-6/5", "days": [1, 2, 3, 4, 5, 6, 7]},
{"value": "CE-6", "text": "龙门币-6/5", "days": [2, 4, 6, 7]},
{"value": "AP-5", "text": "红票-5", "days": [1, 4, 6, 7]},
{"value": "CA-5", "text": "技能-5", "days": [2, 3, 5, 7]},
{"value": "SK-5", "text": "碳-5", "days": [1, 3, 5, 6]},
{"value": "PR-A-1", "text": "奶/盾芯片", "days": [1, 4, 5, 7]},
{"value": "PR-A-2", "text": "奶/盾芯片组", "days": [1, 4, 5, 7]},
{"value": "PR-B-1", "text": "术/狙芯片", "days": [1, 2, 5, 6]},
{"value": "PR-B-2", "text": "术/狙芯片组", "days": [1, 2, 5, 6]},
{"value": "PR-C-1", "text": "先/辅芯片", "days": [3, 4, 6, 7]},
{"value": "PR-C-2", "text": "先/辅芯片组", "days": [3, 4, 6, 7]},
{"value": "PR-D-1", "text": "近/特芯片", "days": [2, 3, 6, 7]},
{"value": "PR-D-2", "text": "近/特芯片组", "days": [2, 3, 6, 7]},
]
"""常规资源关信息"""
RESOURCE_STAGE_DATE_TEXT = {
"LS-6": "经验-6/5 | 常驻开放",
"CE-6": "龙门币-6/5 | 二四六日开放",
"AP-5": "红票-5 | 一四六日开放",
"CA-5": "技能-5 | 二三五日开放",
"SK-5": "碳-5 | 一三五六开放",
"PR-A-1": "奶/盾芯片 | 一四五日开放",
"PR-A-2": "奶/盾芯片组 | 一四五日开放",
"PR-B-1": "术/狙芯片 | 一二五六日开放",
"PR-B-2": "术/狙芯片组 | 一二五六日开放",
"PR-C-1": "先/辅芯片 | 三四六日开放",
"PR-C-2": "先/辅芯片组 | 三四六日开放",
"PR-D-1": "近/特芯片 | 二三六日开放",
"PR-D-2": "近/特芯片组 | 二三六日开放",
}
"""常规资源关开放日文本映射"""
RESOURCE_STAGE_DROP_INFO = {
"CE-6": {
"Display": "CE-6",
"Value": "CE-6",
"Drop": "4001",
"DropName": "龙门币",
"Activity": {"Tip": "二四六日", "StageName": "资源关卡"},
},
"AP-5": {
"Display": "AP-5",
"Value": "AP-5",
"Drop": "4006",
"DropName": "采购凭证",
"Activity": {"Tip": "一四六日", "StageName": "资源关卡"},
},
"CA-5": {
"Display": "CA-5",
"Value": "CA-5",
"Drop": "3303",
"DropName": "技巧概要",
"Activity": {"Tip": "二三五日", "StageName": "资源关卡"},
},
"LS-6": {
"Display": "LS-6",
"Value": "LS-6",
"Drop": "2004",
"DropName": "作战记录",
"Activity": {"Tip": "常驻开放", "StageName": "资源关卡"},
},
"SK-5": {
"Display": "SK-5",
"Value": "SK-5",
"Drop": "3114",
"DropName": "碳素组",
"Activity": {"Tip": "一三五六", "StageName": "资源关卡"},
},
"PR-A-1": {
"Display": "PR-A",
"Value": "PR-A",
"Drop": "PR-A",
"DropName": "奶/盾芯片",
"Activity": {"Tip": "一四五日", "StageName": "资源关卡"},
},
"PR-B-1": {
"Display": "PR-B",
"Value": "PR-B",
"Drop": "PR-B",
"DropName": "术/狙芯片",
"Activity": {"Tip": "一二五六", "StageName": "资源关卡"},
},
"PR-C-1": {
"Display": "PR-C",
"Value": "PR-C",
"Drop": "PR-C",
"DropName": "先/辅芯片",
"Activity": {"Tip": "三四六日", "StageName": "资源关卡"},
},
"PR-D-1": {
"Display": "PR-D",
"Value": "PR-D",
"Drop": "PR-D",
"DropName": "近/特芯片",
"Activity": {"Tip": "二三六日", "StageName": "资源关卡"},
},
}
"""常规资源关掉落信息"""
MATERIALS_MAP = {
"4001": "龙门币",
"4006": "采购凭证",
"2004": "高级作战记录",
"2003": "中级作战记录",
"2002": "初级作战记录",
"2001": "基础作战记录",
"3303": "技巧概要·卷3",
"3302": "技巧概要·卷2",
"3301": "技巧概要·卷1",
"30165": "重相位对映体",
"30155": "烧结核凝晶",
"30145": "晶体电子单元",
"30135": "D32钢",
"30125": "双极纳米片",
"30115": "聚合剂",
"31094": "手性屈光体",
"31093": "类凝结核",
"31084": "环烃预制体",
"31083": "环烃聚质",
"31074": "固化纤维板",
"31073": "褐素纤维",
"31064": "转质盐聚块",
"31063": "转质盐组",
"31054": "切削原液",
"31053": "化合切削液",
"31044": "精炼溶剂",
"31043": "半自然溶剂",
"31034": "晶体电路",
"31033": "晶体元件",
"31024": "炽合金块",
"31023": "炽合金",
"31014": "聚合凝胶",
"31013": "凝胶",
"30074": "白马醇",
"30073": "扭转醇",
"30084": "三水锰矿",
"30083": "轻锰矿",
"30094": "五水研磨石",
"30093": "研磨石",
"30104": "RMA70-24",
"30103": "RMA70-12",
"30014": "提纯源岩",
"30013": "固源岩组",
"30012": "固源岩",
"30011": "源岩",
"30064": "改量装置",
"30063": "全新装置",
"30062": "装置",
"30061": "破损装置",
"30034": "聚酸酯块",
"30033": "聚酸酯组",
"30032": "聚酸酯",
"30031": "酯原料",
"30024": "糖聚块",
"30023": "糖组",
"30022": "",
"30021": "代糖",
"30044": "异铁块",
"30043": "异铁组",
"30042": "异铁",
"30041": "异铁碎片",
"30054": "酮阵列",
"30053": "酮凝集组",
"30052": "酮凝集",
"30051": "双酮",
"3114": "碳素组",
"3113": "碳素",
"3112": "",
"3213": "先锋双芯片",
"3223": "近卫双芯片",
"3233": "重装双芯片",
"3243": "狙击双芯片",
"3253": "术师双芯片",
"3263": "医疗双芯片",
"3273": "辅助双芯片",
"3283": "特种双芯片",
"3212": "先锋芯片组",
"3222": "近卫芯片组",
"3232": "重装芯片组",
"3242": "狙击芯片组",
"3252": "术师芯片组",
"3262": "医疗芯片组",
"3272": "辅助芯片组",
"3282": "特种芯片组",
"3211": "先锋芯片",
"3221": "近卫芯片",
"3231": "重装芯片",
"3241": "狙击芯片",
"3251": "术师芯片",
"3261": "医疗芯片",
"3271": "辅助芯片",
"3281": "特种芯片",
"PR-A": "医疗/重装芯片",
"PR-B": "术师/狙击芯片",
"PR-C": "先锋/辅助芯片",
"PR-D": "近卫/特种芯片",
}
"""掉落物索引表"""
POWER_SIGN_MAP = {
"NoAction": "无动作",
"Shutdown": "关机",
"ShutdownForce": "强制关机",
"Hibernate": "休眠",
"Sleep": "睡眠",
"KillSelf": "退出程序",
}
"""电源操作类型索引表"""
RESERVED_NAMES = {
"CON",
"PRN",
"AUX",
"NUL",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9",
}
"""Windows保留名称列表"""
ILLEGAL_CHARS = set('<>:"/\\|?*')
"""文件名非法字符集合"""
MIRROR_ERROR_INFO = {
1001: "获取版本信息的URL参数不正确",
7001: "填入的 CDK 已过期",
7002: "填入的 CDK 错误",
7003: "填入的 CDK 今日下载次数已达上限",
7004: "填入的 CDK 类型和待下载的资源不匹配",
7005: "填入的 CDK 已被封禁",
8001: "对应架构和系统下的资源不存在",
8002: "错误的系统参数",
8003: "错误的架构参数",
8004: "错误的更新通道参数",
1: "未知错误类型",
}
"""MirrorChyan错误代码映射表"""
DEFAULT_DATETIME = datetime.strptime("2000-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
"""默认日期时间"""

View File

@@ -0,0 +1,5 @@
from .mumu import MumuManager
from .ldplayer import LDManager
from .utils import BaseDevice, DeviceStatus
__all__ = ["MumuManager", "LDManager", "BaseDevice", "DeviceStatus"]

View File

@@ -0,0 +1,492 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import asyncio
import psutil
import subprocess
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, Any
from app.utils.device_manager.utils import BaseDevice, DeviceStatus
from app.utils.logger import get_logger
class ProcessManager:
"""进程监视器类, 用于跟踪主进程及其所有子进程的状态"""
def __init__(self):
super().__init__()
self.main_pid = None
self.tracked_pids = set()
self.check_task = None
self.track_end_time = datetime.now()
async def open_process(
self, path: Path, args: list = [], tracking_time: int = 60
) -> None:
"""
启动一个新进程并返回其pid, 并开始监视该进程
Parameters
----------
path: 可执行文件的路径
args: 启动参数列表
tracking_time: 子进程追踪持续时间(秒)
"""
process = subprocess.Popen(
[path, *args],
cwd=path.parent,
creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
await self.start_monitoring(process.pid, tracking_time)
async def start_monitoring(self, pid: int, tracking_time: int = 60) -> None:
"""
启动进程监视器, 跟踪指定的主进程及其子进程
:param pid: 被监视进程的PID
:param tracking_time: 子进程追踪持续时间(秒)
"""
await self.clear()
self.main_pid = pid
self.tracking_time = tracking_time
# 扫描并记录所有相关进程
try:
# 获取主进程
main_proc = psutil.Process(self.main_pid)
self.tracked_pids.add(self.main_pid)
# 递归获取所有子进程
if tracking_time:
for child in main_proc.children(recursive=True):
self.tracked_pids.add(child.pid)
except psutil.NoSuchProcess:
pass
# 启动持续追踪任务
if tracking_time > 0:
self.track_end_time = datetime.now() + timedelta(seconds=tracking_time)
self.check_task = asyncio.create_task(self.track_processes())
async def track_processes(self) -> None:
"""更新子进程列表"""
while datetime.now() < self.track_end_time:
current_pids = set(self.tracked_pids)
for pid in current_pids:
try:
proc = psutil.Process(pid)
for child in proc.children():
if child.pid not in self.tracked_pids:
# 新发现的子进程
self.tracked_pids.add(child.pid)
except psutil.NoSuchProcess:
continue
await asyncio.sleep(0.1)
async def is_running(self) -> bool:
"""检查所有跟踪的进程是否还在运行"""
for pid in self.tracked_pids:
try:
proc = psutil.Process(pid)
if proc.is_running():
return True
except psutil.NoSuchProcess:
continue
return False
async def kill(self, if_force: bool = False) -> None:
"""停止监视器并中止所有跟踪的进程"""
for pid in self.tracked_pids:
try:
proc = psutil.Process(pid)
if if_force:
kill_process = subprocess.Popen(
["taskkill", "/F", "/T", "/PID", str(pid)],
creationflags=subprocess.CREATE_NO_WINDOW,
)
kill_process.wait()
proc.terminate()
except psutil.NoSuchProcess:
continue
await self.clear()
async def clear(self) -> None:
"""清空跟踪的进程列表"""
if self.check_task is not None and not self.check_task.done():
self.check_task.cancel()
try:
await self.check_task
except asyncio.CancelledError:
pass
self.main_pid = None
self.tracked_pids.clear()
class GeneralDeviceManager(BaseDevice):
"""
通用设备管理器基于BaseDevice和ProcessManager实现
用于管理一般应用程序进程
"""
def __init__(self, executable_path: str, name: str = "通用设备"):
"""
初始化通用设备管理器
Args:
executable_path (str): 可执行文件的绝对路径
name (str): 设备管理器名称
"""
self.executable_path = Path(executable_path)
self.name = name
self.logger = get_logger(f"{name}管理器")
# 进程管理实例字典以idx为键
self.process_managers: Dict[str, ProcessManager] = {}
# 设备信息存储
self.device_info: Dict[str, Dict[str, Any]] = {}
# 默认等待时间
self.wait_time = 60
if not self.executable_path.exists():
raise FileNotFoundError(f"可执行文件不存在: {executable_path}")
async def start(self, idx: str, package_name: str = "") -> tuple[bool, int, dict]:
"""
启动设备
Args:
idx: 设备ID
package_name: 包名(可选)
Returns:
tuple[bool, int, dict]: (是否成功, 状态码, 启动信息)
"""
try:
# 检查是否已经在运行
current_status = await self.get_status(idx)
if current_status in [DeviceStatus.ONLINE, DeviceStatus.STARTING]:
self.logger.warning(f"设备{idx}已经在运行,状态: {current_status}")
return False, current_status, {}
# 创建进程管理器
if idx not in self.process_managers:
self.process_managers[idx] = ProcessManager()
# 准备启动参数
args = []
if package_name:
args.extend(["-pkg", package_name])
# 启动进程
await self.process_managers[idx].open_process(
self.executable_path, args, tracking_time=self.wait_time
)
# 等待进程启动
start_time = datetime.now()
timeout = timedelta(seconds=self.wait_time)
while datetime.now() - start_time < timeout:
if await self.process_managers[idx].is_running():
self.device_info[idx] = {
"title": f"{self.name}_{idx}",
"status": str(DeviceStatus.ONLINE),
"pid": self.process_managers[idx].main_pid,
"start_time": start_time.isoformat(),
}
self.logger.info(f"设备{idx}启动成功")
return True, DeviceStatus.ONLINE, self.device_info[idx]
await asyncio.sleep(0.1)
self.logger.error(f"设备{idx}启动超时")
return False, DeviceStatus.ERROR, {}
except Exception as e:
self.logger.error(f"启动设备{idx}失败: {str(e)}")
return False, DeviceStatus.ERROR, {}
async def close(self, idx: str) -> tuple[bool, int]:
"""
关闭设备或服务
Args:
idx: 设备ID
Returns:
tuple[bool, int]: (是否成功, 状态码)
"""
try:
if idx not in self.process_managers:
self.logger.warning(f"设备{idx}的进程管理器不存在")
return False, DeviceStatus.NOT_FOUND
# 检查进程是否在运行
if not await self.process_managers[idx].is_running():
self.logger.info(f"设备{idx}进程已经停止")
return True, DeviceStatus.OFFLINE
# 终止进程
await self.process_managers[idx].kill(if_force=False)
# 等待进程完全停止
stop_time = datetime.now()
timeout = timedelta(seconds=10) # 10秒超时
while datetime.now() - stop_time < timeout:
if not await self.process_managers[idx].is_running():
# 清理设备信息
if idx in self.device_info:
del self.device_info[idx]
self.logger.info(f"设备{idx}已成功关闭")
return True, DeviceStatus.OFFLINE
await asyncio.sleep(0.1)
# 强制终止
self.logger.warning(f"设备{idx}未能正常关闭,尝试强制终止")
await self.process_managers[idx].kill(if_force=True)
if idx in self.device_info:
del self.device_info[idx]
return True, DeviceStatus.OFFLINE
except Exception as e:
self.logger.error(f"关闭设备{idx}失败: {str(e)}")
return False, DeviceStatus.ERROR
async def get_status(self, idx: str) -> int:
"""
获取指定设备当前状态
Args:
idx: 设备ID
Returns:
int: 状态码
"""
try:
if idx not in self.process_managers:
return DeviceStatus.OFFLINE
if await self.process_managers[idx].is_running():
return DeviceStatus.ONLINE
else:
return DeviceStatus.OFFLINE
except Exception as e:
self.logger.error(f"获取设备{idx}状态失败: {str(e)}")
return DeviceStatus.ERROR
async def hide_device(self, idx: str) -> tuple[bool, int]:
"""
隐藏设备窗口
Args:
idx: 设备ID
Returns:
tuple[bool, int]: (是否成功, 状态码)
"""
try:
status = await self.get_status(idx)
if status != DeviceStatus.ONLINE:
return False, status
if (
idx not in self.process_managers
or not self.process_managers[idx].main_pid
):
return False, DeviceStatus.NOT_FOUND
# 窗口隐藏功能(简化实现)
# 注意完整的窗口隐藏功能需要更复杂的Windows API调用
self.logger.info(f"设备{idx}窗口隐藏请求已处理(简化实现)")
return True, DeviceStatus.ONLINE
self.logger.info(f"设备{idx}窗口已隐藏")
return True, DeviceStatus.ONLINE
except ImportError:
self.logger.warning("隐藏窗口功能需要pywin32库")
return False, DeviceStatus.ERROR
except Exception as e:
self.logger.error(f"隐藏设备{idx}窗口失败: {str(e)}")
return False, DeviceStatus.ERROR
async def show_device(self, idx: str) -> tuple[bool, int]:
"""
显示设备窗口
Args:
idx: 设备ID
Returns:
tuple[bool, int]: (是否成功, 状态码)
"""
try:
status = await self.get_status(idx)
if status != DeviceStatus.ONLINE:
return False, status
if (
idx not in self.process_managers
or not self.process_managers[idx].main_pid
):
return False, DeviceStatus.NOT_FOUND
# 窗口显示功能(简化实现)
# 注意完整的窗口显示功能需要更复杂的Windows API调用
self.logger.info(f"设备{idx}窗口显示请求已处理(简化实现)")
return True, DeviceStatus.ONLINE
self.logger.info(f"设备{idx}窗口已显示")
return True, DeviceStatus.ONLINE
except ImportError:
self.logger.warning("显示窗口功能需要pywin32库")
return False, DeviceStatus.ERROR
except Exception as e:
self.logger.error(f"显示设备{idx}窗口失败: {str(e)}")
return False, DeviceStatus.ERROR
async def get_all_info(self) -> dict[str, dict[str, str]]:
"""
获取所有设备信息
Returns:
dict[str, dict[str, str]]: 设备信息字典
结构示例:
{
"0": {
"title": "设备名称",
"status": "1"
}
}
"""
result = {}
for idx in list(self.process_managers.keys()):
try:
status = await self.get_status(idx)
if idx in self.device_info:
title = self.device_info[idx].get("title", f"{self.name}_{idx}")
else:
title = f"{self.name}_{idx}"
result[idx] = {"title": title, "status": str(status)}
except Exception as e:
self.logger.error(f"获取设备{idx}信息失败: {str(e)}")
result[idx] = {
"title": f"{self.name}_{idx}",
"status": str(DeviceStatus.ERROR),
}
return result
async def cleanup(self) -> None:
"""
清理所有资源
"""
self.logger.info("开始清理设备管理器资源")
for idx, pm in list(self.process_managers.items()):
try:
if await pm.is_running():
await pm.kill(if_force=True)
await pm.clear()
except Exception as e:
self.logger.error(f"清理设备{idx}资源失败: {str(e)}")
self.process_managers.clear()
self.device_info.clear()
self.logger.info("设备管理器资源清理完成")
def __del__(self):
"""析构函数,确保资源被正确释放"""
try:
# 注意析构函数中不能使用async/await
# 这里只是标记实际清理需要显式调用cleanup()
if hasattr(self, "process_managers") and self.process_managers:
self.logger.warning("设备管理器未正确清理请显式调用cleanup()方法")
except: # noqa: E722
pass
# 使用示例
if __name__ == "__main__":
async def main():
# 创建通用设备管理器
manager = GeneralDeviceManager(
executable_path=r"C:\Windows\System32\notepad.exe", name="记事本"
)
try:
# 启动设备
success, status, info = await manager.start("0")
print(f"启动结果: {success}, 状态: {status}, 信息: {info}")
if success:
# 获取所有设备信息
all_info = await manager.get_all_info()
print(f"所有设备信息: {all_info}")
# 等待5秒
await asyncio.sleep(5)
# 关闭设备
close_success, close_status = await manager.close("0")
print(f"关闭结果: {close_success}, 状态: {close_status}")
finally:
# 清理资源
await manager.cleanup()
# 运行示例
asyncio.run(main())

View File

@@ -0,0 +1,278 @@
"""
键盘工具模块
提供虚拟键码到keyboard库按键名称的转换功能以及相关的键盘操作辅助函数。
"""
import asyncio
import keyboard
from typing import List
from app.utils.logger import get_logger
logger = get_logger("键盘工具")
def vk_code_to_key_name(vk_code: int) -> str:
"""
将Windows虚拟键码转换为keyboard库识别的按键名称
Args:
vk_code (int): Windows虚拟键码
Returns:
str: keyboard库识别的按键名称
Examples:
>>> vk_code_to_key_name(0x1B)
'esc'
>>> vk_code_to_key_name(0x70)
'f1'
>>> vk_code_to_key_name(0x41)
'a'
"""
# Windows虚拟键码到keyboard库按键名称的映射
vk_mapping = {
# 常用功能键
0x1B: "esc", # VK_ESCAPE
0x0D: "enter", # VK_RETURN
0x20: "space", # VK_SPACE
0x08: "backspace", # VK_BACK
0x09: "tab", # VK_TAB
0x2E: "delete", # VK_DELETE
0x24: "home", # VK_HOME
0x23: "end", # VK_END
0x21: "page up", # VK_PRIOR
0x22: "page down", # VK_NEXT
0x2D: "insert", # VK_INSERT
# 修饰键
0x10: "shift", # VK_SHIFT
0x11: "ctrl", # VK_CONTROL
0x12: "alt", # VK_MENU
0x5B: "left windows", # VK_LWIN
0x5C: "right windows", # VK_RWIN
0x5D: "apps", # VK_APPS (右键菜单键)
# 方向键
0x25: "left", # VK_LEFT
0x26: "up", # VK_UP
0x27: "right", # VK_RIGHT
0x28: "down", # VK_DOWN
# 功能键 F1-F12
0x70: "f1",
0x71: "f2",
0x72: "f3",
0x73: "f4",
0x74: "f5",
0x75: "f6",
0x76: "f7",
0x77: "f8",
0x78: "f9",
0x79: "f10",
0x7A: "f11",
0x7B: "f12",
# 数字键 0-9
0x30: "0",
0x31: "1",
0x32: "2",
0x33: "3",
0x34: "4",
0x35: "5",
0x36: "6",
0x37: "7",
0x38: "8",
0x39: "9",
# 字母键 A-Z
0x41: "a",
0x42: "b",
0x43: "c",
0x44: "d",
0x45: "e",
0x46: "f",
0x47: "g",
0x48: "h",
0x49: "i",
0x4A: "j",
0x4B: "k",
0x4C: "l",
0x4D: "m",
0x4E: "n",
0x4F: "o",
0x50: "p",
0x51: "q",
0x52: "r",
0x53: "s",
0x54: "t",
0x55: "u",
0x56: "v",
0x57: "w",
0x58: "x",
0x59: "y",
0x5A: "z",
# 数字小键盘
0x60: "num 0",
0x61: "num 1",
0x62: "num 2",
0x63: "num 3",
0x64: "num 4",
0x65: "num 5",
0x66: "num 6",
0x67: "num 7",
0x68: "num 8",
0x69: "num 9",
0x6A: "num *",
0x6B: "num +",
0x6D: "num -",
0x6E: "num .",
0x6F: "num /",
0x90: "num lock",
# 标点符号和特殊键
0xBA: ";", # VK_OEM_1 (;:)
0xBB: "=", # VK_OEM_PLUS (=+)
0xBC: ",", # VK_OEM_COMMA (,<)
0xBD: "-", # VK_OEM_MINUS (-_)
0xBE: ".", # VK_OEM_PERIOD (.>)
0xBF: "/", # VK_OEM_2 (/?)
0xC0: "`", # VK_OEM_3 (`~)
0xDB: "[", # VK_OEM_4 ([{)
0xDC: "\\", # VK_OEM_5 (\|)
0xDD: "]", # VK_OEM_6 (]})
0xDE: "'", # VK_OEM_7 ('")
# 系统键
0x14: "caps lock", # VK_CAPITAL
0x91: "scroll lock", # VK_SCROLL
0x13: "pause", # VK_PAUSE
0x2C: "print screen", # VK_SNAPSHOT
# 媒体键
0xA0: "left shift", # VK_LSHIFT
0xA1: "right shift", # VK_RSHIFT
0xA2: "left ctrl", # VK_LCONTROL
0xA3: "right ctrl", # VK_RCONTROL
0xA4: "left alt", # VK_LMENU
0xA5: "right alt", # VK_RMENU
}
return vk_mapping.get(vk_code, f"vk_{vk_code}")
def vk_codes_to_key_names(vk_codes: List[int]) -> List[str]:
"""
将多个虚拟键码转换为keyboard库识别的按键名称列表
Args:
vk_codes (List[int]): Windows虚拟键码列表
Returns:
List[str]: keyboard库识别的按键名称列表
Examples:
>>> vk_codes_to_key_names([0x11, 0x12, 0x44])
['ctrl', 'alt', 'd']
"""
return [vk_code_to_key_name(vk) for vk in vk_codes]
async def send_key_combination(key_names: List[str], hold_time: float = 0.05) -> bool:
"""
发送按键组合
Args:
key_names (List[str]): 按键名称列表
hold_time (float): 按键保持时间(秒),默认 0.05 秒
Returns:
bool: 操作是否成功
Examples:
>>> await send_key_combination(['ctrl', 'alt', 'd'])
True
>>> await send_key_combination(['f1'])
True
"""
try:
if not key_names:
logger.warning("按键名称列表为空")
return False
if len(key_names) == 1:
# 单个按键
keyboard.press_and_release(key_names[0])
logger.debug(f"发送单个按键: {key_names[0]}")
else:
# 组合键:按下所有键,然后释放
logger.debug(f"发送组合键: {'+'.join(key_names)}")
for key in key_names:
keyboard.press(key)
await asyncio.sleep(hold_time) # 保持按键状态
for key in reversed(key_names):
keyboard.release(key)
return True
except Exception as e:
logger.error(f"发送按键组合失败: {e}")
return False
async def send_vk_combination(vk_codes: List[int], hold_time: float = 0.05) -> bool:
"""
发送虚拟键码组合
Args:
vk_codes (List[int]): Windows虚拟键码列表
hold_time (float): 按键保持时间(秒),默认 0.05 秒
Returns:
bool: 操作是否成功
Examples:
>>> await send_vk_combination([0x11, 0x12, 0x44]) # Ctrl+Alt+D
True
>>> await send_vk_combination([0x70]) # F1
True
"""
try:
key_names = vk_codes_to_key_names(vk_codes)
return await send_key_combination(key_names, hold_time)
except Exception as e:
logger.error(f"发送虚拟键码组合失败: {e}")
return False
def get_common_boss_keys() -> dict[str, List[int]]:
"""
获取常见的BOSS键组合
Returns:
dict[str, List[int]]: 常见BOSS键组合的名称和对应的虚拟键码
"""
return {
"alt_tab": [0x12, 0x09], # Alt+Tab
"ctrl_alt_d": [0x11, 0x12, 0x44], # Ctrl+Alt+D
"win_d": [0x5B, 0x44], # Win+D (显示桌面)
"win_m": [0x5B, 0x4D], # Win+M (最小化所有窗口)
"f1": [0x70], # F1
"f2": [0x71], # F2
"f3": [0x72], # F3
"f4": [0x73], # F4
"alt_f4": [0x12, 0x73], # Alt+F4 (关闭窗口)
"ctrl_shift_esc": [0x11, 0x10, 0x1B], # Ctrl+Shift+Esc (任务管理器)
}
def describe_vk_combination(vk_codes: List[int]) -> str:
"""
描述虚拟键码组合
Args:
vk_codes (List[int]): Windows虚拟键码列表
Returns:
str: 组合键的描述字符串
Examples:
>>> describe_vk_combination([0x11, 0x12, 0x44])
'Ctrl+Alt+D'
>>> describe_vk_combination([0x70])
'F1'
"""
key_names = vk_codes_to_key_names(vk_codes)
return "+".join(name.title() for name in key_names)

View File

@@ -0,0 +1,329 @@
import asyncio
from typing import Literal
from app.utils.device_manager.utils import BaseDevice, ExeRunner, DeviceStatus
from app.utils.logger import get_logger
from app.utils.device_manager.keyboard_utils import (
vk_codes_to_key_names,
send_key_combination,
)
import psutil
from pydantic import BaseModel
import win32gui
class EmulatorInfo(BaseModel):
idx: int
title: str
top_hwnd: int
bind_hwnd: int
in_android: int
pid: int
vbox_pid: int
width: int
height: int
density: int
class LDManager(BaseDevice):
"""
基于dnconsole.exe的模拟器管理
!需要管理员权限
"""
def __init__(self, exe_path: str) -> None:
"""_summary_
Args:
exe_path (str): dnconsole.exe的绝对路径
"""
self.runner = ExeRunner(exe_path, "gbk")
self.logger = get_logger("雷电模拟器管理器")
self.wait_time = 60 # 配置获取 后续改一下 单位为s
async def start(self, idx: str, package_name="") -> tuple[bool, int, dict]:
"""
启动指定模拟器
Returns:
tuple[bool, int, str]: 是否成功, 当前状态码, ADB端口信息
"""
OK, info, status = await self.get_device_info(idx)
if status != DeviceStatus.OFFLINE:
self.logger.error(
f"模拟器{idx}未处于关闭状态,当前状态码: {status}, 需求状态码: {DeviceStatus.OFFLINE}"
)
return False, status, {}
if package_name:
result = self.runner.run(
"launch",
"--index",
idx,
"--packagename",
f'"{package_name}"',
)
else:
result = self.runner.run(
"launch",
"--index",
idx,
)
# 参考命令 dnconsole.exe launch --index 0
self.logger.debug(f"启动结果:{result}")
if result.returncode != 0:
raise RuntimeError(f"命令执行失败: {result}")
i = 0
while i < self.wait_time * 10:
OK, info, status = await self.get_device_info(idx)
if status == DeviceStatus.ERROR or status == DeviceStatus.UNKNOWN:
self.logger.error(f"模拟器{idx}启动失败,状态码: {status}")
return False, status, {}
if status == DeviceStatus.ONLINE:
self.logger.debug(info)
if OK and isinstance(info, EmulatorInfo):
pid: int = info.vbox_pid
adb_port = ""
adb_host_ip = await self.get_adb_ports(pid)
print(adb_host_ip)
if adb_host_ip:
return (
True,
status,
{"adb_port": adb_port, "adb_host_ip": adb_host_ip},
)
return True, status, {}
await asyncio.sleep(0.1)
i += 1
return False, DeviceStatus.UNKNOWN, {}
async def close(self, idx: str) -> tuple[bool, int]:
"""
关闭指定模拟器
Returns:
- tuple[bool, int]: 是否成功, 当前状态码
参考命令行:dnconsole.exe quit --index 0
"""
OK, info, status = await self.get_device_info(idx)
if status != DeviceStatus.ONLINE and status != DeviceStatus.STARTING:
return False, DeviceStatus.NOT_FOUND
result = self.runner.run(
"quit",
"--index",
idx,
)
# 参考命令 dnconsole.exe quit --index 0
if result.returncode != 0:
return True, DeviceStatus.OFFLINE
i = 0
while i < self.wait_time * 10:
OK, info, status = await self.get_device_info(idx)
if status == DeviceStatus.ERROR or status == DeviceStatus.UNKNOWN:
return False, status
if status == DeviceStatus.OFFLINE:
return True, DeviceStatus.OFFLINE
await asyncio.sleep(0.1)
i += 1
return False, DeviceStatus.UNKNOWN
async def get_status(self, idx: str) -> int:
"""
获取指定模拟器当前状态
返回值: 状态码
"""
_, _, status = await self.get_device_info(idx)
return status
async def get_device_info(
self,
idx: str,
data: dict[int, EmulatorInfo] | None = None,
) -> tuple[Literal[True], EmulatorInfo, int] | tuple[Literal[False], dict, int]:
"""
获取指定模拟器的信息和状态
Returns:
- tuple[bool, EmulatorInfo | dict, int]: 是否成功, 模拟器信息或空字典, 状态码
参考命令行:dnconsole.exe list2
"""
if not data:
result = await self._get_all_info()
else:
result = data
try:
emulator_info = result.get(int(idx))
print(emulator_info)
if not emulator_info:
self.logger.error(f"未找到模拟器{idx}的信息")
return False, {}, DeviceStatus.UNKNOWN
self.logger.debug(f"获取模拟器{idx}信息: {emulator_info}")
# 计算状态码
if emulator_info.in_android == 1:
status = DeviceStatus.ONLINE
elif emulator_info.in_android == 2:
if emulator_info.vbox_pid > 0:
status = DeviceStatus.STARTING
# 雷电启动后, vbox_pid为-1, 目前不知道有什么区别
else:
status = DeviceStatus.STARTING
elif emulator_info.in_android == 0:
status = DeviceStatus.OFFLINE
else:
status = DeviceStatus.UNKNOWN
self.logger.debug(f"获取模拟器{idx}状态: {status}")
return True, emulator_info, status
except: # noqa: E722
self.logger.error(f"获取模拟器{idx}信息失败")
return False, {}, DeviceStatus.UNKNOWN
async def _get_all_info(self) -> dict[int, EmulatorInfo]:
result = self.runner.run("list2")
# self.logger.debug(f"全部信息{result.stdout.strip()}")
if result.returncode != 0:
raise RuntimeError(f"命令执行失败: {result}")
emulators: dict[int, EmulatorInfo] = {}
data = result.stdout.strip()
for line in data.strip().splitlines():
parts = line.strip().split(",")
if len(parts) != 10:
raise ValueError(f"数据格式错误: {line}")
try:
info = EmulatorInfo(
idx=int(parts[0]),
title=parts[1],
top_hwnd=int(parts[2]),
bind_hwnd=int(parts[3]),
in_android=int(parts[4]),
pid=int(parts[5]),
vbox_pid=int(parts[6]),
width=int(parts[7]),
height=int(parts[8]),
density=int(parts[9]),
)
emulators[info.idx] = info
except Exception as e:
self.logger.warning(f"解析失败: {line}, 错误: {e}")
pass
return emulators
# ?wk雷电你都返回了什么啊
async def get_all_info(self) -> dict[str, dict[str, str]]:
"""
解析_emulator_info字典提取idx和title便于前端显示
"""
raw_data = await self._get_all_info()
result: dict[str, dict[str, str]] = {}
for info in raw_data.values():
OK, device_info, status = await self.get_device_info(
str(info.idx), raw_data
)
result[str(info.idx)] = {"title": info.title, "status": str(status)}
return result
async def send_boss_key(
self,
boss_keys: list[int],
result: EmulatorInfo,
is_show: bool = False,
# True: 显示, False: 隐藏
) -> bool:
"""
发送BOSS键
Args:
idx (str): 模拟器索引
boss_keys (list[int]): BOSS键的虚拟键码列表
result (EmulatorInfo): 模拟器信息
is_show (bool, optional): 将要隐藏或显示窗口,默认为 False隐藏
"""
hwnd = result.top_hwnd
try:
# 使用键盘工具发送按键组合
success = await send_key_combination(vk_codes_to_key_names(boss_keys))
if not success:
return False
# 等待系统处理
await asyncio.sleep(0.5)
# 检查窗口可见性是否符合预期
current_visible = win32gui.IsWindowVisible(hwnd)
expected_visible = is_show
if current_visible == expected_visible:
return True
else:
# 如果第一次没有成功,再试一次
success = await send_key_combination(vk_codes_to_key_names(boss_keys))
if not success:
return False
await asyncio.sleep(0.5)
current_visible = win32gui.IsWindowVisible(hwnd)
return current_visible == expected_visible
except Exception as e:
self.logger.error(f"发送BOSS键失败: {e}")
return False
async def hide_device(
self,
idx: str,
boss_keys: list[int] = [],
) -> tuple[bool, int]:
"""隐藏设备窗口"""
OK, result, status = await self.get_device_info(idx)
if not OK or not isinstance(result, EmulatorInfo):
return False, DeviceStatus.UNKNOWN
if status != DeviceStatus.ONLINE:
return False, status
return await self.send_boss_key(boss_keys, result, False), status
async def show_device(
self,
idx: str,
boss_keys: list[int] = [],
) -> tuple[bool, int]:
"""显示设备窗口"""
OK, result, status = await self.get_device_info(idx)
if not OK or not isinstance(result, EmulatorInfo):
return False, DeviceStatus.UNKNOWN
if status != DeviceStatus.ONLINE:
return False, status
return await self.send_boss_key(boss_keys, result, True), status
async def get_adb_ports(self, pid: int) -> int:
"""使用psutil获取adb端口"""
try:
process = psutil.Process(pid)
connections = process.connections(kind="inet")
for conn in connections:
if conn.status == psutil.CONN_LISTEN and conn.laddr.port != 2222:
return conn.laddr.port
return 0 # 如果没有找到合适的端口返回0
except: # noqa: E722
return 0
if __name__ == "__main__":
MANAGER_PATH = (
r"C:\leidian\LDPlayer9\dnconsole.exe" # 替换为实际的dnconsole.exe路径
)
idx = "0" # 替换为实际存在的模拟器实例索
manager = LDManager(MANAGER_PATH)
# asyncio.run(manager._get_all_info())
a = asyncio.run(manager.start("0"))
print(a)

View File

@@ -0,0 +1,220 @@
import asyncio
import json
from app.utils.device_manager.utils import BaseDevice, ExeRunner, DeviceStatus
from app.utils.logger import get_logger
class MumuManager(BaseDevice):
"""
基于MuMuManager.exe的模拟器管理
"""
def __init__(self, exe_path: str) -> None:
"""_summary_
Args:
exe_path (str): MuMuManager.exe的绝对路径
"""
self.runner = ExeRunner(exe_path, "utf-8")
self.logger = get_logger("MuMu管理器")
self.wait_time = 60 # 配置获取 后续改一下 单位为s
async def start(self, idx: str, package_name="") -> tuple[bool, int, dict]:
"""
启动指定模拟器
Returns:
tuple[bool, int, str]: 是否成功, 当前状态码, ADB端口信息
"""
status = await self.get_status(idx)
if status != DeviceStatus.OFFLINE:
self.logger.error(
f"模拟器{idx}未处于关闭状态,当前状态码: {status}, 需求状态码: {DeviceStatus.OFFLINE}"
)
return False, status, {}
if package_name:
result = self.runner.run(
"control",
"-v",
idx,
"launch",
"-pkg",
package_name,
)
else:
result = self.runner.run(
"control",
"-v",
idx,
"launch",
)
# 参考命令 MuMuManager.exe control -v 2 launch
self.logger.debug(f"启动结果:{result}")
if result.returncode != 0:
raise RuntimeError(f"命令执行失败: {result}")
i = 0
while i < self.wait_time * 10:
status = await self.get_status(idx)
if status == DeviceStatus.ERROR or status == DeviceStatus.UNKNOWN:
self.logger.error(f"模拟器{idx}启动失败,状态码: {status}")
return False, status, {}
if status == DeviceStatus.ONLINE:
OK, info = await self.get_device_info(idx)
self.logger.debug(info)
if OK:
data = json.loads(info)
adb_port = data.get("adb_port")
adb_host_ip = data.get("adb_host_ip")
if adb_port and adb_host_ip:
return (
True,
status,
{"adb_port": adb_port, "adb_host_ip": adb_host_ip},
)
return True, status, {}
await asyncio.sleep(0.1)
i += 1
return False, DeviceStatus.UNKNOWN, {}
async def close(self, idx: str) -> tuple[bool, int]:
"""
关闭指定模拟器
Returns:
tuple[bool, int]: 是否成功, 当前状态码
"""
status = await self.get_status(idx)
if status != DeviceStatus.ONLINE and status != DeviceStatus.STARTING:
return False, DeviceStatus.NOT_FOUND
result = self.runner.run(
"control",
"-v",
idx,
"shutdown",
)
# 参考命令 MuMuManager.exe control -v 2 shutdown
if result.returncode != 0:
return True, DeviceStatus.OFFLINE
i = 0
while i < self.wait_time * 10:
status = await self.get_status(idx)
if status == DeviceStatus.ERROR or status == DeviceStatus.UNKNOWN:
return False, status
if status == DeviceStatus.OFFLINE:
return True, DeviceStatus.OFFLINE
await asyncio.sleep(0.1)
i += 1
return False, DeviceStatus.UNKNOWN
async def get_status(self, idx: str, data: str | None = None) -> int:
if not data:
OK, result_str = await self.get_device_info(idx)
self.logger.debug(f"获取状态结果{result_str}")
else:
OK, result_str = True, data
try:
result_json = json.loads(result_str)
if OK:
if result_json["is_android_started"]:
return DeviceStatus.STARTING
elif result_json["is_process_started"]:
return DeviceStatus.ONLINE
else:
return DeviceStatus.OFFLINE
else:
if result_json["errmsg"] == "unknown error":
return DeviceStatus.UNKNOWN
else:
return DeviceStatus.ERROR
except json.JSONDecodeError as e:
self.logger.error(f"JSON解析错误: {e}")
return DeviceStatus.UNKNOWN
async def get_device_info(self, idx: str) -> tuple[bool, str]:
result = self.runner.run(
"info",
"-v",
idx,
)
self.logger.debug(f"获取模拟器{idx}信息: {result}")
if result.returncode != 0:
return False, result.stdout.strip()
else:
return True, result.stdout.strip()
async def _get_all_info(self) -> str:
result = self.runner.run(
"info",
"-v",
"all",
)
# self.logger.debug(f"result{result.stdout.strip()}")
if result.returncode != 0:
raise RuntimeError(f"命令执行失败: {result}")
return result.stdout.strip()
async def get_all_info(self) -> dict[str, dict[str, str]]:
json_data = await self._get_all_info()
data = json.loads(json_data)
result: dict[str, dict[str, str]] = {}
if not data:
return result
if isinstance(data, dict) and "index" in data and "name" in data:
index = data["index"]
name = data["name"]
status = self.get_status(index, json_data)
result[index] = {
"title": name,
"status": str(status),
}
elif isinstance(data, dict):
for key, value in data.items():
if isinstance(value, dict) and "index" in value and "name" in value:
index = value["index"]
name = value["name"]
status = await self.get_status(index)
result[index] = {
"title": name,
"status": str(status),
}
return result
async def hide_device(self, idx: str) -> tuple[bool, int]:
"""隐藏设备窗口"""
status = await self.get_status(idx)
if status != DeviceStatus.ONLINE:
return False, status
result = self.runner.run(
"control",
"-v",
idx,
"hide_window",
)
if result.returncode != 0:
return False, status
return True, DeviceStatus.ONLINE
async def show_device(self, idx: str) -> tuple[bool, int]:
"""显示设备窗口"""
status = await self.get_status(idx)
if status != DeviceStatus.ONLINE:
return False, status
result = self.runner.run(
"control",
"-v",
idx,
"show_window",
)
if result.returncode != 0:
return False, status
return True, DeviceStatus.ONLINE

View File

@@ -0,0 +1,104 @@
import subprocess
import os
from abc import ABC, abstractmethod
from enum import IntEnum
class DeviceStatus(IntEnum):
ONLINE = 0
"""设备在线"""
OFFLINE = 1
"""设备离线"""
STARTING = 2
"""设备开启中"""
CLOSEING = 3
"""设备关闭中"""
ERROR = 4
"""错误"""
NOT_FOUND = 5
"""未找到设备"""
UNKNOWN = 10
class ExeRunner:
def __init__(self, exe_path, encoding) -> None:
"""
指定 exe 路径
!请传入绝对路径,使用/分隔路径
"""
if not os.path.isfile(exe_path):
raise FileNotFoundError(f"找不到文件: {exe_path}")
self.exe_path = os.path.abspath(exe_path) # 转为绝对路径
self.encoding = encoding
def run(self, *args) -> subprocess.CompletedProcess[str]:
"""
执行命令,返回结果
"""
cmd = [self.exe_path] + list(args)
print(f"执行: {' '.join(cmd)}")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding=self.encoding,
errors="replace",
)
return result
class BaseDevice(ABC):
@abstractmethod
async def start(self, idx: str, package_name: str) -> tuple[bool, int, dict]:
"""
启动设备
返回值: (是否成功, 状态码, 启动信息)
"""
...
@abstractmethod
async def close(self, idx: str) -> tuple[bool, int]:
"""
关闭设备或服务
返回值: (是否成功, 状态码)
"""
...
@abstractmethod
async def get_status(self, idx: str) -> int:
"""
获取指定模拟器当前状态
返回值: 状态码
"""
...
@abstractmethod
async def hide_device(self, idx: str) -> tuple[bool, int]:
"""
隐藏设备窗口
返回值: (是否成功, 状态码)
"""
...
@abstractmethod
async def show_device(self, idx: str) -> tuple[bool, int]:
"""
显示设备窗口
返回值: (是否成功, 状态码)
"""
...
async def get_all_info(self) -> dict[str, dict[str, str]]:
"""
获取设备信息
返回值: 设备字典
结构示例:
{
"0":{
"title": 模拟器名字,
"status": "1"
}
}
"""
...

70
app/utils/logger.py Normal file
View File

@@ -0,0 +1,70 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 MoeSnowyFox
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
from loguru import logger as _logger
import sys
from pathlib import Path
(Path.cwd() / "debug").mkdir(parents=True, exist_ok=True)
_logger.remove()
_logger.add(
sink=Path.cwd() / "debug/app.log",
level="INFO",
format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{extra[module]}</cyan> | <level>{message}</level>",
enqueue=True,
backtrace=True,
diagnose=True,
rotation="1 week",
retention="1 month",
compression="zip",
)
_logger.add(
sink=sys.stderr,
level="DEBUG",
format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{extra[module]}</cyan> | <level>{message}</level>",
enqueue=True,
backtrace=True,
diagnose=True,
colorize=True,
)
_logger = _logger.patch(lambda record: record["extra"].setdefault("module", "未知模块"))
def get_logger(module_name: str):
"""
获取一个绑定 module 名的日志器
:param module_name: 模块名称, 如 "用户管理"
:return: 绑定后的 logger
"""
return _logger.bind(module=module_name)
__all__ = ["get_logger"]

70
app/utils/security.py Normal file
View File

@@ -0,0 +1,70 @@
# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software
# Copyright © 2024-2025 DLmaster361
# Copyright © 2025 AUTO-MAS Team
# This file is part of AUTO-MAS.
# AUTO-MAS 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-MAS 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-MAS. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import base64
import win32crypt
def dpapi_encrypt(
note: str, description: None | str = None, entropy: None | bytes = None
) -> str:
"""
使用Windows DPAPI加密数据
:param note: 数据明文
:type note: str
:param description: 描述信息
:type description: str
:param entropy: 随机熵
:type entropy: bytes
:return: 加密后的数据
:rtype: str
"""
if note == "":
return ""
encrypted = win32crypt.CryptProtectData(
note.encode("utf-8"), description, entropy, None, None, 0
)
return base64.b64encode(encrypted).decode("utf-8")
def dpapi_decrypt(note: str, entropy: None | bytes = None) -> str:
"""
使用Windows DPAPI解密数据
:param note: 数据密文
:type note: str
:param entropy: 随机熵
:type entropy: bytes
:return: 解密后的明文
:rtype: str
"""
if note == "":
return ""
decrypted = win32crypt.CryptUnprotectData(
base64.b64decode(note), entropy, None, None, 0
)
return decrypted[1].decode("utf-8")

View File

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

View File

@@ -0,0 +1,411 @@
# AUTO-MAS 后端任务调度逻辑与WebSocket消息格式说明
## 1. 任务调度架构概览
AUTO-MAS 后端采用基于 AsyncIO 的异步任务调度系统,主要由以下核心组件构成:
### 1.1 核心组件
- **TaskManager**: 任务调度器,负责任务的创建、运行、停止和清理
- **Broadcast**: 消息广播系统,负责在不同组件间传递消息
- **WebSocket**: 与前端的实时通信通道
- **Config**: 配置管理系统,包含脚本配置、队列配置等
### 1.2 任务类型
系统支持三种主要任务模式:
1. **设置脚本** - 直接执行单个脚本配置
2. **自动代理** - 按队列顺序自动执行多个脚本
3. **人工排查** - 手动排查和执行任务
## 2. 任务调度流程
### 2.1 任务创建流程
```
前端请求 → API接口 → TaskManager.add_task() → 任务验证 → 创建异步任务 → 返回任务ID
```
**具体步骤:**
1. **任务验证**: 根据模式和UID验证任务配置是否存在
2. **重复检查**: 确保相同任务未在运行中
3. **任务创建**: 使用`asyncio.create_task()`创建异步任务
4. **回调设置**: 添加任务完成回调用于清理工作
### 2.2 任务执行流程
#### 设置脚本模式
```
获取脚本配置 → 确定脚本类型(MAA/General) → 创建对应Manager → 执行任务
```
#### 自动代理模式
```
获取队列配置 → 构建任务列表 → 逐个执行脚本 → 更新状态 → 发送完成信号
```
### 2.3 任务状态管理
- **等待**: 任务已加入队列但未开始执行
- **运行**: 任务正在执行中
- **跳过**: 任务因重复或其他原因被跳过
- **完成**: 任务执行完毕
## 3. WebSocket 消息系统
### 3.1 消息基础结构
所有WebSocket消息都遵循统一的JSON格式
```json
{
"id": "消息ID或任务ID",
"type": "消息类型",
"data": {
"具体数据": "根据类型而定"
}
}
```
### 3.2 消息类型详解
#### 3.2.1 Update 类型 - 数据更新
**用途**: 通知前端更新界面数据,"user_list"仅给出当前处于`运行`状态的脚本的用户列表值
**常见数据格式:**
```json
{
"id": "task-uuid",
"type": "Update",
"data": {
"user_list": [
{
"name": "用户名",
"status": "运行状态",
"config": "配置信息"
}
]
}
}
```
```json
{
"id": "task-uuid",
"type": "Update",
"data": {
"task_dict": [
{
"script_id": "脚本ID",
"status": "等待/运行/完成/跳过",
"name": "脚本名称",
"user_list": [
{
"name": "用户名",
"status": "运行状态",
"config": "配置信息"
}
]
}
]
}
}
```
```json
{
"id": "task-uuid",
"type": "Update",
"data": {
"task_list": [
{
"script_id": "脚本ID",
"status": "等待/运行/完成/跳过",
"name": "脚本名称"
}
]
}
}
```
```json
{
"id": "task-uuid",
"type": "Update",
"data": {
"log": "任务执行日志内容"
}
}
```
#### 3.2.2 Info 类型 - 信息显示
**用途**: 向前端发送需要显示的信息,包括普通信息、警告和错误
**数据格式:**
```json
{
"id": "task-uuid",
"type": "Info",
"data": {
"Error": "错误信息内容"
}
}
```
```json
{
"id": "task-uuid",
"type": "Info",
"data": {
"Warning": "警告信息内容"
}
}
```
```json
{
"id": "task-uuid",
"type": "Info",
"data": {
"Info": "普通信息内容"
}
}
```
#### 3.2.3 Message 类型 - 对话框请求
**用途**: 请求前端弹出对话框显示重要信息
**数据格式:**
```json
{
"id": "task-uuid",
"type": "Message",
"data": {
"title": "对话框标题",
"content": "对话框内容",
"type": "info/warning/error"
}
}
```
#### 3.2.4 Signal 类型 - 程序信号
**用途**: 发送程序控制信号和状态通知
**常见信号:**
**任务完成信号:**
```json
{
"id": "task-uuid",
"type": "Signal",
"data": {
"Accomplish": "任务完成后调度台显示的日志内容"
}
}
```
**电源操作信号:**
```json
{
"id": "task-uuid",
"type": "Signal",
"data": {
"power": "NoAction/KillSelf/Sleep/Hibernate/Shutdown/ShutdownForce",
}
}
```
**心跳信号:**
```json
{
"id": "Main",
"type": "Signal",
"data": {
"Ping": "无描述"
}
}
```
```json
{
"id": "Main",
"type": "Signal",
"data": {
"Pong": "无描述"
}
}
```
## 4. 任务管理器详细说明
### 4.1 TaskManager 核心方法
#### add_task(mode: str, uid: str)
- **功能**: 添加新任务到调度队列
- **参数**:
- `mode`: 任务模式 ("设置脚本", "自动代理", "人工排查")
- `uid`: 任务唯一标识符
- **返回**: 任务UUID
#### stop_task(task_id: str)
- **功能**: 停止指定任务
- **参数**:
- `task_id`: 任务ID支持 "ALL" 停止所有任务
#### run_task(mode: str, task_id: UUID, actual_id: Optional[UUID])
- **功能**: 执行具体任务逻辑
- **流程**: 根据模式选择相应的执行策略
### 4.2 任务执行器
#### GeneralManager
- **用途**: 处理通用脚本任务
- **特点**: 支持自定义脚本路径和参数
- **配置**: 基于 GeneralConfig 和 GeneralUserConfig
#### MaaManager
- **用途**: 处理MAA (明日方舟助手) 专用任务
- **特点**: 支持模拟器控制、ADB连接、游戏自动化
- **配置**: 基于 MaaConfig 和 MaaUserConfig
## 5. 消息广播系统
### 5.1 Broadcast 机制
- **设计模式**: 发布-订阅模式
- **功能**: 实现组件间解耦的消息传递
- **特点**: 支持多个订阅者同时接收消息
### 5.2 消息流向
```
任务执行器 → Broadcast → WebSocket → 前端界面
其他订阅者
```
## 6. 配置管理系统
### 6.1 配置类型
- **ScriptConfig**: 脚本配置包含MAA和General两种类型
- **QueueConfig**: 队列配置,定义自动代理任务的执行顺序
- **GlobalConfig**: 全局系统配置
### 6.2 配置操作
- **锁机制**: 防止配置在使用时被修改
- **实时更新**: 支持动态加载配置变更
- **类型验证**: 确保配置数据的正确性
## 7. API 接口说明
### 7.1 任务控制接口
**创建任务**
- **端点**: `POST /api/dispatch/start`
- **请求体**:
```json
{
"mode": "自动代理|人工排查|设置脚本",
"taskId": "目标任务ID"
}
```
- **响应**:
```json
{
"code": 200,
"status": "success",
"message": "操作成功",
"websocketId": "新任务ID"
}
```
**停止任务**
- **端点**: `POST /api/dispatch/stop`
- **请求体**:
```json
{
"taskId": "要停止的任务ID"
}
```
**电源操作**
- **端点**: `POST /api/dispatch/power`
- **请求体**:
```json
{
"signal": "NoAction|Shutdown|ShutdownForce|Hibernate|Sleep|KillSelf"
}
```
### 7.2 WebSocket 连接
**端点**: `WS /api/core/ws`
**连接特性**:
- 同时只允许一个WebSocket连接
- 自动心跳检测 (15秒超时)
- 连接断开时自动清理资源
## 8. 错误处理机制
### 8.1 异常类型
- **ValueError**: 配置验证失败
- **RuntimeError**: 任务状态冲突
- **TimeoutError**: 操作超时
- **ConnectionError**: 连接相关错误
### 8.2 错误响应格式
```json
{
"id": "相关任务ID",
"type": "Info",
"data": {
"Error": "具体错误描述"
}
}
```
## 9. 性能和监控
### 9.1 日志系统
- **分层日志**: 按模块划分日志记录器
- **实时监控**: 支持日志实时推送到前端
- **文件轮转**: 自动管理日志文件大小
### 9.2 资源管理
- **进程管理**: 自动清理子进程
- **内存监控**: 防止内存泄漏
- **连接池**: 复用数据库和网络连接
## 10. 安全考虑
### 10.1 输入验证
- **参数校验**: 使用 Pydantic 模型验证
- **路径安全**: 防止路径遍历攻击
- **命令注入**: 严格控制执行的命令参数
### 10.2 权限控制
- **单一连接**: 限制WebSocket连接数量
- **操作限制**: 防止重复或冲突操作
- **资源保护**: 防止资源滥用
---
*此文档基于 AUTO-MAS v5.0.0 版本编写详细的API文档和配置说明请参考相关配置文件和源代码注释。*

View File

@@ -0,0 +1,126 @@
# TaskManager WebSocket消息处理功能实现
## 功能概述
根据后端TaskManager的WebSocket消息机制实现了前端对ID为"TaskManager"的WebSocket消息的完整处理逻辑。
## 后端TaskManager消息分析
### 消息格式
```json
{
"id": "TaskManager",
"type": "Signal",
"data": {
"newTask": "任务UUID"
}
}
```
### 触发时机
当后端启动时运行的队列开始执行时TaskManager会发送此消息通知前端有新任务被自动创建。
## 前端实现
### 1. WebSocket订阅机制
`useSchedulerLogic.ts`中添加了以下功能:
- **subscribeToTaskManager()**: 订阅ID为"TaskManager"的WebSocket消息
- **handleTaskManagerMessage()**: 处理TaskManager发送的消息
- **createSchedulerTabForTask()**: 根据任务ID自动创建调度台
### 2. 自动调度台创建逻辑
当收到`newTask`信号时,系统会:
1. **检查重复**: 验证是否已存在相同websocketId的调度台
2. **创建调度台**: 自动创建新的调度台标签页
3. **设置状态**: 直接将调度台状态设置为"运行"
4. **建立连接**: 立即订阅该任务的WebSocket消息
5. **用户提示**: 显示成功创建的消息提示
### 3. 调度台特性
自动创建的调度台具有以下特性:
- 标题格式:`自动调度台{编号}`
- 初始状态:`运行`
- 可关闭:`true`(但运行时不可删除)
- 自动订阅:立即开始接收任务消息
### 4. 生命周期管理
- **初始化**: 在组件挂载时调用`initialize()`订阅TaskManager消息
- **清理**: 在组件卸载时取消TaskManager订阅
- **任务结束**: 复用现有的任务结束处理逻辑
## 代码修改点
### 1. useSchedulerLogic.ts
```typescript
// 新增TaskManager消息订阅
const subscribeToTaskManager = () => {
ws.subscribe('TaskManager', {
onMessage: (message) => handleTaskManagerMessage(message)
})
}
// 新增TaskManager消息处理
const handleTaskManagerMessage = (wsMessage: any) => {
if (type === 'Signal' && data && data.newTask) {
createSchedulerTabForTask(data.newTask)
}
}
// 新增自动调度台创建
const createSchedulerTabForTask = (taskId: string) => {
// 创建运行状态的调度台并立即订阅
}
```
### 2. index.vue
```typescript
// 生命周期中添加初始化调用
onMounted(() => {
initialize() // 订阅TaskManager消息
loadTaskOptions()
})
```
## 功能特点
### 1. 无缝集成
- 完全复用现有的调度台逻辑和UI组件
- 与手动创建的调度台行为一致
- 支持所有现有功能(日志显示、任务总览、消息处理等)
### 2. 状态同步
- 调度台状态与后端任务状态严格同步
- 支持任务完成后的自动状态更新
- 正确处理WebSocket连接的建立和清理
### 3. 用户体验
- 自动切换到新创建的调度台
- 提供清晰的成功提示
- 防止重复创建相同任务的调度台
### 4. 错误处理
- 检查消息格式的有效性
- 防止重复订阅和创建
- 优雅处理异常情况
## 测试验证
功能实现后需要验证以下场景:
1. **启动时队列**: 后端启动时运行的队列应自动创建调度台
2. **消息接收**: 调度台应正确接收和显示任务消息
3. **状态更新**: 任务状态变化应正确反映在UI上
4. **任务结束**: 任务完成后应正确清理资源
5. **重复处理**: 相同任务不应创建多个调度台
## 兼容性
- 完全向后兼容现有功能
- 不影响手动创建的调度台
- 保持现有的WebSocket消息处理机制
- 复用所有现有的UI组件和样式

View File

@@ -0,0 +1,427 @@
# useWebSocket API 参考文档
## 概述
`useWebSocket()` 组合式函数提供了完整的 WebSocket 通信接口,包含消息订阅、连接管理、状态监控等核心功能。
## 导出函数详解
### 1. subscribe() - 订阅消息
```typescript
const subscribe = (
filter: SubscriptionFilter,
handler: (message: WebSocketBaseMessage) => void
): string
```
#### 参数说明
**filter: SubscriptionFilter**
```typescript
interface SubscriptionFilter {
type?: string // 消息类型过滤器(可选)
id?: string // 消息ID过滤器可选
needCache?: boolean // 是否启用缓存回放(可选)
}
```
**handler: Function**
- 消息处理回调函数
- 参数: `message: WebSocketBaseMessage`
- 无返回值
#### 返回值
- `string`: 唯一的订阅ID用于后续取消订阅
#### 使用示例
```typescript
// 订阅所有消息
const allMsgSub = subscribe({}, (msg) => {
console.log('收到消息:', msg)
})
// 订阅特定类型消息
const taskSub = subscribe(
{ type: 'TaskUpdate', needCache: true },
(msg) => {
console.log('任务更新:', msg.data)
}
)
// 订阅特定ID消息
const specificSub = subscribe(
{ id: 'TaskManager' },
(msg) => {
console.log('任务管理器消息:', msg)
}
)
// 精确订阅同时匹配type和id
const preciseSub = subscribe(
{ type: 'Progress', id: 'task_001', needCache: true },
(msg) => {
console.log('特定任务进度:', msg.data)
}
)
```
#### 特殊功能
- **自动回放**: 如果 `needCache: true`,会立即回放匹配的历史消息
- **过滤优先级**: type + id > type > id > 全部
- **引用计数**: 多个订阅共享缓存,自动管理内存
---
### 2. unsubscribe() - 取消订阅
```typescript
const unsubscribe = (subscriptionId: string): void
```
#### 参数说明
**subscriptionId: string**
-`subscribe()` 返回的订阅ID
- 必须是有效的订阅ID
#### 使用示例
```typescript
const subId = subscribe({ type: 'TaskUpdate' }, handleTaskUpdate)
// 取消订阅
unsubscribe(subId)
// Vue 组件中的最佳实践
import { onUnmounted } from 'vue'
const setupSubscription = () => {
const subId = subscribe({ type: 'TaskUpdate' }, handleTaskUpdate)
onUnmounted(() => {
unsubscribe(subId)
})
}
```
#### 自动清理
- 自动减少缓存引用计数
- 引用计数为0时清理相关缓存
- 不会影响其他订阅者
---
### 3. sendRaw() - 发送消息
```typescript
const sendRaw = (type: string, data?: any, id?: string): void
```
#### 参数说明
**type: string** (必需)
- 消息类型标识
- 后端用于路由消息
**data: any** (可选)
- 消息负载数据
- 可以是任何可序列化的对象
**id: string** (可选)
- 消息标识符
- 用于消息跟踪和响应匹配
#### 使用示例
```typescript
// 发送简单消息
sendRaw('Hello')
// 发送带数据的消息
sendRaw('TaskStart', {
taskId: '12345',
config: { timeout: 30000 }
})
// 发送带ID的消息便于追踪响应
sendRaw('GetTaskStatus', { taskId: '12345' }, 'query_001')
// 发送控制信号
sendRaw('Signal', {
command: 'pause',
reason: '用户手动暂停'
}, 'TaskManager')
```
#### 发送条件
- 仅在 WebSocket 连接为 `OPEN` 状态时发送
- 连接异常时静默失败(不抛出异常)
- 自动JSON序列化
---
### 4. getConnectionInfo() - 获取连接信息
```typescript
const getConnectionInfo = (): ConnectionInfo
```
#### 返回值类型
```typescript
interface ConnectionInfo {
connectionId: string // 连接唯一标识
status: WebSocketStatus // 当前连接状态
subscriberCount: number // 当前订阅者数量
moduleLoadCount: number // 模块加载计数
wsReadyState: number | null // WebSocket原生状态
isConnecting: boolean // 是否正在连接
hasHeartbeat: boolean // 是否启用心跳
hasEverConnected: boolean // 是否曾经连接成功
reconnectAttempts: number // 重连尝试次数
isPersistentMode: boolean // 是否持久化模式
}
```
#### 使用示例
```typescript
const info = getConnectionInfo()
console.log('连接ID:', info.connectionId)
console.log('连接状态:', info.status)
console.log('订阅者数量:', info.subscriberCount)
// 检查连接是否健康
const isHealthy = info.status === '已连接' &&
info.hasHeartbeat &&
info.wsReadyState === WebSocket.OPEN
// 监控重连情况
if (info.reconnectAttempts > 0) {
console.log(`已重连 ${info.reconnectAttempts} 次`)
}
```
#### 调试用途
- 诊断连接问题
- 监控连接质量
- 统计使用情况
---
### 5. status - 连接状态
```typescript
const status: Ref<WebSocketStatus>
```
#### 状态类型
```typescript
type WebSocketStatus = '连接中' | '已连接' | '已断开' | '连接错误'
```
#### 状态说明
| 状态 | 描述 | 触发条件 |
|------|------|----------|
| `'连接中'` | 正在建立连接 | WebSocket.CONNECTING |
| `'已连接'` | 连接成功 | WebSocket.OPEN |
| `'已断开'` | 连接断开 | WebSocket.CLOSED |
| `'连接错误'` | 连接异常 | WebSocket.onerror |
#### 使用示例
```typescript
import { watch } from 'vue'
const { status } = useWebSocket()
// 监听状态变化
watch(status, (newStatus) => {
console.log('连接状态变化:', newStatus)
switch (newStatus) {
case '已连接':
console.log('✅ WebSocket 连接成功')
break
case '已断开':
console.log('❌ WebSocket 连接断开')
break
case '连接错误':
console.log('⚠️ WebSocket 连接错误')
break
case '连接中':
console.log('🔄 WebSocket 连接中...')
break
}
})
// 在模板中显示状态
// <div>连接状态: {{ status }}</div>
```
#### 响应式特性
- Vue 响应式 Ref 对象
- 自动更新 UI
- 可用于计算属性和监听器
---
### 6. backendStatus - 后端状态
```typescript
const backendStatus: Ref<BackendStatus>
```
#### 状态类型
```typescript
type BackendStatus = 'unknown' | 'starting' | 'running' | 'stopped' | 'error'
```
#### 状态说明
| 状态 | 描述 | 含义 |
|------|------|------|
| `'unknown'` | 未知状态 | 初始状态,尚未检测 |
| `'starting'` | 启动中 | 后端服务正在启动 |
| `'running'` | 运行中 | 后端服务正常运行 |
| `'stopped'` | 已停止 | 后端服务已停止 |
| `'error'` | 错误状态 | 后端服务异常 |
#### 使用示例
```typescript
const { backendStatus, restartBackend } = useWebSocket()
// 监听后端状态
watch(backendStatus, (newStatus) => {
console.log('后端状态:', newStatus)
switch (newStatus) {
case 'running':
console.log('✅ 后端服务运行正常')
break
case 'stopped':
console.log('⏹️ 后端服务已停止')
break
case 'error':
console.log('❌ 后端服务异常')
// 可以提示用户或自动重启
break
case 'starting':
console.log('🚀 后端服务启动中...')
break
}
})
// 根据状态显示不同UI
const statusColor = computed(() => {
switch (backendStatus.value) {
case 'running': return 'green'
case 'error': return 'red'
case 'starting': return 'orange'
default: return 'gray'
}
})
```
#### 自动管理
- 每3秒自动检测一次
- 异常时自动尝试重启最多3次
- 集成 Electron 进程管理
---
## 完整使用示例
```typescript
import { onMounted, onUnmounted, watch } from 'vue'
import { useWebSocket } from '@/composables/useWebSocket'
export default {
setup() {
const {
subscribe,
unsubscribe,
sendRaw,
getConnectionInfo,
status,
backendStatus
} = useWebSocket()
let taskSubscription: string
let systemSubscription: string
onMounted(() => {
// 订阅任务消息
taskSubscription = subscribe(
{ type: 'TaskUpdate', needCache: true },
(message) => {
console.log('任务更新:', message.data)
}
)
// 订阅系统消息
systemSubscription = subscribe(
{ id: 'System' },
(message) => {
console.log('系统消息:', message)
}
)
// 发送初始化消息
sendRaw('ClientReady', {
timestamp: Date.now()
}, 'System')
})
onUnmounted(() => {
// 清理订阅
if (taskSubscription) unsubscribe(taskSubscription)
if (systemSubscription) unsubscribe(systemSubscription)
})
// 监听连接状态
watch([status, backendStatus], ([wsStatus, beStatus]) => {
console.log(`WS: ${wsStatus}, Backend: ${beStatus}`)
})
// 获取连接信息
const connectionInfo = getConnectionInfo()
return {
status,
backendStatus,
connectionInfo,
sendMessage: (type: string, data: any) => sendRaw(type, data)
}
}
}
```
## 最佳实践
### 1. 订阅管理
- 总是在组件卸载时取消订阅
- 使用 `needCache: true` 确保不丢失消息
- 避免重复订阅相同的消息类型
### 2. 错误处理
- 监听连接状态变化
- 根据后端状态调整UI显示
- 实现重连提示和手动重启
### 3. 性能优化
- 精确的过滤条件减少不必要的处理
- 合理使用缓存避免消息丢失
- 及时取消不需要的订阅
### 4. 调试技巧
- 使用 `getConnectionInfo()` 诊断问题
- 开发环境下查看控制台日志
- 监控订阅者数量避免内存泄漏

View File

@@ -0,0 +1,336 @@
## 核心架构设计
### 1. 全局持久化存储
```typescript
const WS_STORAGE_KEY = Symbol.for('GLOBAL_WEBSOCKET_PERSISTENT')
```
- 使用 `Symbol.for()` 确保全局唯一性
- 存储在 `window` 对象上,实现跨组件共享
- 避免多次实例化,确保连接唯一性
### 2. 状态管理结构
```typescript
interface GlobalWSStorage {
wsRef: WebSocket | null // WebSocket 实例
status: Ref<WebSocketStatus> // 连接状态
subscriptions: Ref<Map<string, WebSocketSubscription>> // 订阅管理
cacheMarkers: Ref<Map<string, CacheMarker>> // 缓存标记
cachedMessages: Ref<Array<CachedMessage>> // 消息缓存
// ... 其他状态
}
```
## 核心功能模块
### 1. 配置管理
```typescript
const BASE_WS_URL = 'ws://localhost:36163/api/core/ws'
const HEARTBEAT_INTERVAL = 15000 // 心跳间隔
const HEARTBEAT_TIMEOUT = 5000 // 心跳超时
const BACKEND_CHECK_INTERVAL = 3000 // 后端检查间隔
const MAX_RESTART_ATTEMPTS = 3 // 最大重启尝试次数
const RESTART_DELAY = 2000 // 重启延迟
const MAX_QUEUE_SIZE = 50 // 最大队列大小
const MESSAGE_TTL = 60000 // 消息过期时间
```
**要点**:
- 所有时间配置使用毫秒为单位
- 可根据网络环境调整超时时间
- 队列大小限制防止内存泄漏
### 2. 消息订阅系统
#### 订阅过滤器
```typescript
interface SubscriptionFilter {
type?: string // 消息类型过滤
id?: string // 消息ID过滤
needCache?: boolean // 是否需要缓存
}
```
#### 订阅机制
```typescript
export const subscribe = (
filter: SubscriptionFilter,
handler: (message: WebSocketBaseMessage) => void
): string => {
// 1. 生成唯一订阅ID
// 2. 创建订阅记录
// 3. 添加缓存标记
// 4. 回放匹配的缓存消息
}
```
**要点**:
- 支持按 `type``id` 的组合过滤
- 自动回放缓存消息,确保不丢失历史数据
- 返回订阅ID用于后续取消订阅
### 3. 智能缓存系统
#### 缓存标记机制
```typescript
interface CacheMarker {
type?: string
id?: string
refCount: number // 引用计数
}
```
#### 缓存策略
- **引用计数**: 订阅时 +1取消订阅时 -1
- **自动清理**: 引用计数为 0 时删除标记
- **TTL机制**: 消息超过 60 秒自动过期
- **大小限制**: 每个队列最多保留 50 条消息
### 4. 心跳检测机制
```typescript
const startGlobalHeartbeat = (ws: WebSocket) => {
global.heartbeatTimer = window.setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
const pingTime = Date.now()
global.lastPingTime = pingTime
ws.send(JSON.stringify({
type: 'Signal',
data: { Ping: pingTime, connectionId: global.connectionId }
}))
}
}, HEARTBEAT_INTERVAL)
}
```
### 5. 后端服务监控
#### 状态检测
```typescript
type BackendStatus = 'unknown' | 'starting' | 'running' | 'stopped' | 'error'
```
#### 自动重启逻辑
```typescript
const restartBackend = async (): Promise<boolean> => {
// 1. 防重入检查
// 2. 递增重启计数
// 3. 调用 Electron API 启动后端
// 4. 更新状态
}
```
### 6. 连接控制机制
#### 连接权限控制
```typescript
const allowedConnectionReasons = ['后端启动后连接', '后端重启后重连']
const checkConnectionPermission = () => getGlobalStorage().allowNewConnection
```
#### 连接锁机制
```typescript
let isGlobalConnectingLock = false
const acquireConnectionLock = () => {
if (isGlobalConnectingLock) return false
isGlobalConnectingLock = true
return true
}
```
**AI 开发要点**:
- 防止并发连接导致的竞态条件
- 只允许特定原因的连接请求
- 确保全局唯一连接
## 消息流处理
### 1. 消息匹配算法
```typescript
const messageMatchesFilter = (message: WebSocketBaseMessage, filter: SubscriptionFilter): boolean => {
// 如果都不指定,匹配所有消息
if (!filter.type && !filter.id) return true
// 如果只指定type
if (filter.type && !filter.id) return message.type === filter.type
// 如果只指定id
if (!filter.type && filter.id) return message.id === filter.id
// 如果同时指定type和id必须都匹配
return message.type === filter.type && message.id === filter.id
}
```
### 2. 消息分发流程
```
WebSocket 接收消息
JSON 解析
遍历所有订阅者
匹配过滤条件
调用处理器函数
检查是否需要缓存
添加到缓存队列
```
## 外部接口设计
### 1. 主要导出函数
```typescript
export function useWebSocket() {
return {
subscribe, // 订阅消息
unsubscribe, // 取消订阅
sendRaw, // 发送消息
getConnectionInfo, // 获取连接信息
status, // 连接状态
backendStatus, // 后端状态
restartBackend, // 重启后端
getBackendStatus, // 获取后端状态
}
}
```
### 2. 特殊接口
```typescript
export const connectAfterBackendStart = async (): Promise<boolean>
```
- 后端启动后的连接入口
- 启动后端监控
- 设置连接权限
## 错误处理策略
### 1. 连接错误处理
- WebSocket 连接失败时设置状态为 '连接错误'
- 通过后端监控检测服务状态
- 自动重启后端服务
### 2. 消息处理错误
- 订阅处理器异常时记录警告但不中断其他订阅者
- JSON 解析失败时静默忽略
- 发送消息失败时静默处理
### 3. 后端故障处理
```typescript
const handleBackendFailure = async () => {
if (global.backendRestartAttempts >= MAX_RESTART_ATTEMPTS) {
// 显示错误对话框,提示重启应用
Modal.error({
title: '后端服务异常',
content: '后端服务多次重启失败,请重启整个应用程序。'
})
return
}
// 自动重启逻辑
}
```
## 调试和监控
### 1. 调试模式
```typescript
const DEBUG = process.env.NODE_ENV === 'development'
const log = (...args: any[]) => {
if (DEBUG) console.log('[WebSocket]', ...args)
}
```
### 2. 连接信息监控
```typescript
const getConnectionInfo = () => ({
connectionId: global.connectionId,
status: global.status.value,
subscriberCount: global.subscriptions.value.size,
moduleLoadCount: global.moduleLoadCount,
wsReadyState: global.wsRef ? global.wsRef.readyState : null,
isConnecting: global.isConnecting,
hasHeartbeat: !!global.heartbeatTimer,
hasEverConnected: global.hasEverConnected,
reconnectAttempts: global.reconnectAttempts,
isPersistentMode: true,
})
```
## AI 开发建议
### 1. 使用模式
```typescript
// 在 Vue 组件中使用
const { subscribe, unsubscribe, sendRaw, status } = useWebSocket()
// 订阅特定类型消息
const subId = subscribe(
{ type: 'TaskUpdate', needCache: true },
(message) => {
console.log('收到任务更新:', message.data)
}
)
// 组件卸载时取消订阅
onUnmounted(() => {
unsubscribe(subId)
})
```
### 2. 扩展建议
- 添加消息重试机制
- 实现消息优先级队列
- 支持消息压缩
- 添加连接质量监控
### 3. 性能优化点
- 使用 `Object.freeze()` 冻结配置对象
- 考虑使用 Web Worker 处理大量消息
- 实现消息批处理机制
- 添加消息去重功能
### 4. 安全考虑
- 验证消息来源
- 实现消息签名机制
- 添加连接认证
- 防止消息注入攻击
## 依赖关系
### 1. 外部依赖
- `vue`: 响应式系统和组合式API
- `ant-design-vue`: UI组件库Modal
- `schedulerHandlers`: 默认消息处理器
### 2. 运行时依赖
- `window.electronAPI`: Electron主进程通信
- WebSocket API: 浏览器原生支持
## 总结
这个 WebSocket 组合式函数是一个功能完整、设计精良的实时通信解决方案。它不仅解决了基本的 WebSocket 连接问题,还提供了高级功能如智能缓存、自动重连、后端监控等。
**核心优势**:
1. 全局持久化连接,避免重复建立
2. 智能订阅系统,支持精确过滤
3. 自动缓存回放,确保数据完整性
4. 完善的错误处理和自动恢复
5. 详细的调试和监控信息
**适用场景**:
- 实时数据展示
- 任务状态监控
- 系统通知推送
- 双向通信应用

2
frontend/.env Normal file
View File

@@ -0,0 +1,2 @@
VITE_APP_ENV='prod'
VITE_APP_VERSION='1.0.1'

View File

@@ -0,0 +1,2 @@
VITE_APP_ENV='dev'
VITE_APP_VERSION='0.9.0'

28
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.kiro
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
dist-electron
.yarn
.yarnrc.yml

3
frontend/.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
dist
node_modules
dist-electron

9
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,9 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "avoid"
}

1
frontend/.yarnrc.yml Normal file
View File

@@ -0,0 +1 @@
nodeLinker: node-modules

60
frontend/README.md Normal file
View File

@@ -0,0 +1,60 @@
# AUTO-MAS Frontend
基于 Vue 3 + TypeScript + Ant Design Vue + Electron 的桌面应用程序。
## 功能特性
- 🎨 使用 Ant Design Vue 组件库
- 🌙 支持深色模式(跟随系统/深色/浅色)
- 🎨 支持多种主题色切换
- 📱 响应式侧边栏布局
- 🔧 内置开发者工具
- ⚡ 基于 Vite 的快速开发体验
## 项目结构
```
src/
├── components/ # 组件
│ └── AppLayout.vue # 主布局组件
├── views/ # 页面
│ ├── Home.vue # 主页
│ ├── Scripts.vue # 脚本管理
│ ├── Plans.vue # 计划管理
│ ├── Queue.vue # 调度队列
│ ├── Scheduler.vue # 调度中心
│ ├── History.vue # 历史记录
│ └── Settings.vue # 设置页面
├── router/ # 路由配置
├── composables/ # 组合式函数
│ └── useTheme.ts # 主题管理
└── main.ts # 应用入口
```
## 开发
### 安装依赖
```bash
yarn install
```
### 开发模式
直接打开electron窗口
```bash
yarn dev
```
### 构建
```bash
yarn build
```
## 技术栈
- **前端框架**: Vue 3 + TypeScript
- **UI 组件库**: Ant Design Vue 4.x
- **图标**: @ant-design/icons-vue
- **路由**: Vue Router 4
- **构建工具**: Vite
- **桌面端**: Electron
- **包管理**: Yarn

1404
frontend/electron/main.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
import { contextBridge, ipcRenderer } from 'electron'
window.addEventListener('DOMContentLoaded', () => {
console.log('预加载脚本已加载')
})
// 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
openDevTools: () => ipcRenderer.invoke('open-dev-tools'),
selectFolder: () => ipcRenderer.invoke('select-folder'),
selectFile: (filters?: any[]) => ipcRenderer.invoke('select-file', filters),
openUrl: (url: string) => ipcRenderer.invoke('open-url', url),
// 窗口控制
windowMinimize: () => ipcRenderer.invoke('window-minimize'),
windowMaximize: () => ipcRenderer.invoke('window-maximize'),
windowClose: () => ipcRenderer.invoke('window-close'),
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
appQuit: () => ipcRenderer.invoke('app-quit'),
// 进程管理
getRelatedProcesses: () => ipcRenderer.invoke('get-related-processes'),
killAllProcesses: () => ipcRenderer.invoke('kill-all-processes'),
forceExit: () => ipcRenderer.invoke('force-exit'),
// 初始化相关API
checkEnvironment: () => ipcRenderer.invoke('check-environment'),
checkCriticalFiles: () => ipcRenderer.invoke('check-critical-files'),
downloadPython: (mirror?: string) => ipcRenderer.invoke('download-python', mirror),
installPip: () => ipcRenderer.invoke('install-pip'),
downloadGit: () => ipcRenderer.invoke('download-git'),
checkGitUpdate: () => ipcRenderer.invoke('check-git-update'),
installDependencies: (mirror?: string) => ipcRenderer.invoke('install-dependencies', mirror),
cloneBackend: (repoUrl?: string) => ipcRenderer.invoke('clone-backend', repoUrl),
updateBackend: (repoUrl?: string) => ipcRenderer.invoke('update-backend', repoUrl),
startBackend: () => ipcRenderer.invoke('start-backend'),
stopBackend: () => ipcRenderer.invoke('stop-backend'),
// 管理员权限相关
checkAdmin: () => ipcRenderer.invoke('check-admin'),
restartAsAdmin: () => ipcRenderer.invoke('restart-as-admin'),
// 配置文件操作
saveConfig: (config: any) => ipcRenderer.invoke('save-config', config),
loadConfig: () => ipcRenderer.invoke('load-config'),
resetConfig: () => ipcRenderer.invoke('reset-config'),
// 托盘设置实时更新
updateTraySettings: (uiSettings: any) => ipcRenderer.invoke('update-tray-settings', uiSettings),
// 日志文件操作
getLogPath: () => ipcRenderer.invoke('get-log-path'),
getLogFiles: () => ipcRenderer.invoke('get-log-files'),
getLogs: (lines?: number, fileName?: string) => ipcRenderer.invoke('get-logs', lines, fileName),
clearLogs: (fileName?: string) => ipcRenderer.invoke('clear-logs', fileName),
cleanOldLogs: (daysToKeep?: number) => ipcRenderer.invoke('clean-old-logs', daysToKeep),
// 保留原有方法以兼容现有代码
saveLogsToFile: (logs: string) => ipcRenderer.invoke('save-logs-to-file', logs),
loadLogsFromFile: () => ipcRenderer.invoke('load-logs-from-file'),
// 文件系统操作
openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath),
showItemInFolder: (filePath: string) => ipcRenderer.invoke('show-item-in-folder', filePath),
// 对话框相关
showQuestionDialog: (questionData: any) => ipcRenderer.invoke('show-question-dialog', questionData),
dialogResponse: (messageId: string, choice: boolean) => ipcRenderer.invoke('dialog-response', messageId, choice),
resizeDialogWindow: (height: number) => ipcRenderer.invoke('resize-dialog-window', height),
moveWindow: (deltaX: number, deltaY: number) => ipcRenderer.invoke('move-window', deltaX, deltaY),
// 主题信息获取
getThemeInfo: () => ipcRenderer.invoke('get-theme-info'),
getTheme: () => ipcRenderer.invoke('get-theme'),
// 监听下载进度
onDownloadProgress: (callback: (progress: any) => void) => {
ipcRenderer.on('download-progress', (_, progress) => callback(progress))
},
removeDownloadProgressListener: () => {
ipcRenderer.removeAllListeners('download-progress')
},
})

View File

@@ -0,0 +1,62 @@
import * as https from 'https'
import * as fs from 'fs'
import { BrowserWindow } from 'electron'
import * as http from 'http'
let mainWindow: BrowserWindow | null = null
export function setMainWindow(window: BrowserWindow) {
mainWindow = window
}
export function downloadFile(url: string, outputPath: string): Promise<void> {
return new Promise((resolve, reject) => {
console.log(`开始下载文件: ${url}`)
console.log(`保存路径: ${outputPath}`)
const file = fs.createWriteStream(outputPath)
// 创建HTTP客户端兼容https和http
const client = url.startsWith('https') ? https : http
client
.get(url, response => {
const totalSize = parseInt(response.headers['content-length'] || '0', 10)
let downloadedSize = 0
console.log(`文件大小: ${totalSize} bytes`)
response.pipe(file)
response.on('data', chunk => {
downloadedSize += chunk.length
const progress = totalSize ? Math.round((downloadedSize / totalSize) * 100) : 0
console.log(`下载进度: ${progress}% (${downloadedSize}/${totalSize})`)
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
progress,
status: 'downloading',
message: `下载中... ${progress}%`,
})
}
})
file.on('finish', () => {
file.close()
console.log(`文件下载完成: ${outputPath}`)
resolve()
})
file.on('error', err => {
console.error(`文件写入错误: ${err.message}`)
fs.unlink(outputPath, () => {}) // 删除不完整的文件
reject(err)
})
})
.on('error', err => {
console.error(`下载错误: ${err.message}`)
reject(err)
})
})
}

View File

@@ -0,0 +1,34 @@
import * as path from 'path'
import * as fs from 'fs'
import { app } from 'electron'
// 获取应用根目录
export function getAppRoot(): string {
return process.env.NODE_ENV === 'development' ? process.cwd() : path.dirname(app.getPath('exe'))
}
// 检查环境
export function checkEnvironment(appRoot: string) {
const environmentPath = path.join(appRoot, 'environment')
const pythonPath = path.join(environmentPath, 'python')
const gitPath = path.join(environmentPath, 'git')
const backendPath = path.join(appRoot, 'backend')
const requirementsPath = path.join(backendPath, 'requirements.txt')
const pythonExists = fs.existsSync(pythonPath)
const gitExists = fs.existsSync(gitPath)
const backendExists = fs.existsSync(backendPath)
// 检查依赖是否已安装简单检查是否存在site-packages目录
const sitePackagesPath = path.join(pythonPath, 'Lib', 'site-packages')
const dependenciesInstalled =
fs.existsSync(sitePackagesPath) && fs.readdirSync(sitePackagesPath).length > 10
return {
pythonExists,
gitExists,
backendExists,
dependenciesInstalled,
isInitialized: pythonExists && gitExists && backendExists && dependenciesInstalled,
}
}

View File

@@ -0,0 +1,636 @@
import * as path from 'path'
import * as fs from 'fs'
import { spawn } from 'child_process'
import { BrowserWindow, app } from 'electron'
import AdmZip from 'adm-zip'
import { downloadFile } from './downloadService'
let mainWindow: BrowserWindow | null = null
export function setMainWindow(window: BrowserWindow) {
mainWindow = window
}
const gitDownloadUrl = 'https://download.auto-mas.top/d/AUTO_MAS/git.zip'
// 获取应用版本号
function getAppVersion(appRoot: string): string {
console.log('=== 开始获取应用版本号 ===')
console.log(`应用根目录: ${appRoot}`)
try {
// 方法1: 从 Electron app 获取版本号(打包后可用)
try {
const appVersion = app.getVersion()
if (appVersion && appVersion !== '1.0.0') { // 避免使用默认版本
console.log(`✅ 从 app.getVersion() 获取版本号: ${appVersion}`)
return appVersion
}
} catch (error) {
console.log('⚠️ app.getVersion() 获取失败:', error)
}
// 方法2: 从预设的环境变量获取(如果在构建时注入了)
if (process.env.VITE_APP_VERSION) {
console.log(`✅ 从环境变量获取版本号: ${process.env.VITE_APP_VERSION}`)
return process.env.VITE_APP_VERSION
}
// 方法3: 开发环境下从 package.json 获取
const packageJsonPath = path.join(appRoot, 'frontend', 'package.json')
console.log(`尝试读取前端package.json: ${packageJsonPath}`)
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
const version = packageJson.version || '获取版本失败!'
console.log(`✅ 从前端package.json获取版本号: ${version}`)
return version
}
console.log('⚠️ 前端package.json不存在尝试读取根目录package.json')
// 方法4: 从根目录 package.json 获取(开发环境)
const currentPackageJsonPath = path.join(appRoot, 'package.json')
console.log(`尝试读取根目录package.json: ${currentPackageJsonPath}`)
if (fs.existsSync(currentPackageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(currentPackageJsonPath, 'utf8'))
const version = packageJson.version || '获取版本失败!'
console.log(`✅ 从根目录package.json获取版本号: ${version}`)
return version
}
console.log('❌ 未找到任何版本信息源')
return '获取版本失败!'
} catch (error) {
console.error('❌ 获取版本号失败:', error)
return '获取版本失败!'
}
}
// 检查分支是否存在
async function checkBranchExists(
gitPath: string,
gitEnv: any,
repoUrl: string,
branchName: string
): Promise<boolean> {
console.log(`=== 检查分支是否存在: ${branchName} ===`)
console.log(`Git路径: ${gitPath}`)
console.log(`仓库URL: ${repoUrl}`)
try {
return new Promise<boolean>(resolve => {
const proc = spawn(gitPath, ['ls-remote', '--heads', repoUrl, branchName], {
stdio: 'pipe',
env: gitEnv,
})
let output = ''
let errorOutput = ''
proc.stdout?.on('data', data => {
const chunk = data.toString()
output += chunk
console.log(`git ls-remote stdout: ${chunk.trim()}`)
})
proc.stderr?.on('data', data => {
const chunk = data.toString()
errorOutput += chunk
console.log(`git ls-remote stderr: ${chunk.trim()}`)
})
proc.on('close', code => {
console.log(`git ls-remote 退出码: ${code}`)
// 如果输出包含分支名,说明分支存在
const branchExists = output.includes(`refs/heads/${branchName}`)
console.log(`分支 ${branchName} ${branchExists ? '✅ 存在' : '❌ 不存在'}`)
if (errorOutput) {
console.log(`错误输出: ${errorOutput}`)
}
resolve(branchExists)
})
proc.on('error', error => {
console.error(`git ls-remote 进程错误:`, error)
resolve(false)
})
})
} catch (error) {
console.error(`❌ 检查分支 ${branchName} 时出错:`, error)
return false
}
}
// 递归复制目录,包括文件和隐藏文件
function copyDirSync(src: string, dest: string) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true })
}
const entries = fs.readdirSync(src, { withFileTypes: true })
for (const entry of entries) {
const srcPath = path.join(src, entry.name)
const destPath = path.join(dest, entry.name)
if (entry.isDirectory()) {
copyDirSync(srcPath, destPath)
} else {
// 直接覆盖写,不需要先删除
fs.copyFileSync(srcPath, destPath)
}
}
}
// 获取Git环境变量配置
function getGitEnvironment(appRoot: string) {
const gitDir = path.join(appRoot, 'environment', 'git')
const binPath = path.join(gitDir, 'bin')
const mingw64BinPath = path.join(gitDir, 'mingw64', 'bin')
const gitCorePath = path.join(gitDir, 'mingw64', 'libexec', 'git-core')
return {
...process.env,
// 修复remote-https问题的关键确保所有Git相关路径都在PATH中
PATH: `${binPath};${mingw64BinPath};${gitCorePath};${process.env.PATH}`,
GIT_EXEC_PATH: gitCorePath,
GIT_TEMPLATE_DIR: path.join(gitDir, 'mingw64', 'share', 'git-core', 'templates'),
HOME: process.env.USERPROFILE || process.env.HOME,
// // SSL证书路径
// GIT_SSL_CAINFO: path.join(gitDir, 'mingw64', 'ssl', 'certs', 'ca-bundle.crt'),
// 禁用系统Git配置
GIT_CONFIG_NOSYSTEM: '1',
// 禁用交互式认证
GIT_TERMINAL_PROMPT: '0',
GIT_ASKPASS: '',
// // 修复remote-https问题的关键环境变量
// CURL_CA_BUNDLE: path.join(gitDir, 'mingw64', 'ssl', 'certs', 'ca-bundle.crt'),
// 确保Git能找到所有必要的程序
GIT_HTTP_LOW_SPEED_LIMIT: '0',
GIT_HTTP_LOW_SPEED_TIME: '0',
}
}
// 检查是否为Git仓库
function isGitRepository(dirPath: string): boolean {
const gitDir = path.join(dirPath, '.git')
return fs.existsSync(gitDir)
}
// 下载Git
export async function downloadGit(appRoot: string): Promise<{ success: boolean; error?: string }> {
try {
const environmentPath = path.join(appRoot, 'environment')
const gitPath = path.join(environmentPath, 'git')
if (!fs.existsSync(environmentPath)) {
fs.mkdirSync(environmentPath, { recursive: true })
}
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'git',
progress: 0,
status: 'downloading',
message: '开始下载Git...',
})
}
// 使用自定义Git压缩包
const zipPath = path.join(environmentPath, 'git.zip')
await downloadFile(gitDownloadUrl, zipPath)
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'git',
progress: 100,
status: 'extracting',
message: '正在解压Git...',
})
}
// 解压Git到临时目录然后移动到正确位置
console.log(`开始解压Git到: ${gitPath}`)
// 创建临时解压目录
const tempExtractPath = path.join(environmentPath, 'git_temp')
if (!fs.existsSync(tempExtractPath)) {
fs.mkdirSync(tempExtractPath, { recursive: true })
console.log(`创建临时解压目录: ${tempExtractPath}`)
}
// 解压到临时目录
const zip = new AdmZip(zipPath)
zip.extractAllTo(tempExtractPath, true)
console.log(`Git解压到临时目录: ${tempExtractPath}`)
// 检查解压后的目录结构
const tempContents = fs.readdirSync(tempExtractPath)
console.log(`临时目录内容:`, tempContents)
// 如果解压后有git子目录则从git子目录移动内容
let sourceDir = tempExtractPath
if (tempContents.length === 1 && tempContents[0] === 'git') {
sourceDir = path.join(tempExtractPath, 'git')
console.log(`检测到git子目录使用源目录: ${sourceDir}`)
}
// 确保目标Git目录存在
if (!fs.existsSync(gitPath)) {
fs.mkdirSync(gitPath, { recursive: true })
console.log(`创建Git目录: ${gitPath}`)
}
// 移动文件到最终目录
const sourceContents = fs.readdirSync(sourceDir)
for (const item of sourceContents) {
const sourcePath = path.join(sourceDir, item)
const targetPath = path.join(gitPath, item)
// 如果目标已存在,先删除
if (fs.existsSync(targetPath)) {
if (fs.statSync(targetPath).isDirectory()) {
fs.rmSync(targetPath, { recursive: true, force: true })
} else {
fs.unlinkSync(targetPath)
}
}
// 移动文件或目录
fs.renameSync(sourcePath, targetPath)
console.log(`移动: ${sourcePath} -> ${targetPath}`)
}
// 清理临时目录
fs.rmSync(tempExtractPath, { recursive: true, force: true })
console.log(`清理临时目录: ${tempExtractPath}`)
console.log(`Git解压完成到: ${gitPath}`)
// 删除zip文件
fs.unlinkSync(zipPath)
console.log(`删除临时文件: ${zipPath}`)
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'git',
progress: 100,
status: 'completed',
message: 'Git安装完成',
})
}
return { success: true }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'git',
progress: 0,
status: 'error',
message: `Git下载失败: ${errorMessage}`,
})
}
return { success: false, error: errorMessage }
}
}
// 克隆后端代码(替换原有核心逻辑)
export async function cloneBackend(
appRoot: string,
repoUrl = 'https://github.com/AUTO-MAS-Project/AUTO-MAS.git'
): Promise<{
success: boolean
error?: string
}> {
console.log('=== 开始克隆/更新后端代码 ===')
console.log(`应用根目录: ${appRoot}`)
console.log(`仓库URL: ${repoUrl}`)
try {
const backendPath = appRoot
const gitPath = path.join(appRoot, 'environment', 'git', 'bin', 'git.exe')
console.log(`Git可执行文件路径: ${gitPath}`)
console.log(`后端代码路径: ${backendPath}`)
if (!fs.existsSync(gitPath)) {
const error = `Git可执行文件不存在: ${gitPath}`
console.error(`${error}`)
throw new Error(error)
}
console.log('✅ Git可执行文件存在')
const gitEnv = getGitEnvironment(appRoot)
console.log('✅ Git环境变量配置完成')
// 检查 git 是否可用
console.log('=== 检查Git是否可用 ===')
await new Promise<void>((resolve, reject) => {
const proc = spawn(gitPath, ['--version'], { env: gitEnv })
proc.stdout?.on('data', data => {
console.log(`git --version output: ${data.toString().trim()}`)
})
proc.stderr?.on('data', data => {
console.log(`git --version error: ${data.toString().trim()}`)
})
proc.on('close', code => {
console.log(`git --version 退出码: ${code}`)
if (code === 0) {
console.log('✅ Git可用')
resolve()
} else {
console.error('❌ Git无法正常运行')
reject(new Error('git 无法正常运行'))
}
})
proc.on('error', error => {
console.error('❌ Git进程启动失败:', error)
reject(error)
})
})
// 获取版本号并确定目标分支
const version = getAppVersion(appRoot)
console.log(`=== 分支选择逻辑 ===`)
console.log(`当前应用版本: ${version}`)
let targetBranch = 'feature/refactor' // 默认分支
console.log(`默认分支: ${targetBranch}`)
if (version !== '获取版本失败!') {
// 检查版本对应的分支是否存在
console.log(`开始检查版本分支是否存在...`)
const versionBranchExists = await checkBranchExists(gitPath, gitEnv, repoUrl, version)
if (versionBranchExists) {
targetBranch = version
console.log(`🎯 将使用版本分支: ${targetBranch}`)
} else {
console.log(`⚠️ 版本分支 ${version} 不存在,使用默认分支: ${targetBranch}`)
}
} else {
console.log('⚠️ 版本号获取失败,使用默认分支: feature/refactor')
}
console.log(`=== 最终选择分支: ${targetBranch} ===`)
// 检查是否为Git仓库
const isRepo = isGitRepository(backendPath)
console.log(`检查是否为Git仓库: ${isRepo ? '✅ 是' : '❌ 否'}`)
// ==== 下面是关键逻辑 ====
if (isRepo) {
console.log('=== 更新现有Git仓库 ===')
// 已是 git 仓库先更新远程URL为镜像站然后 pull
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'backend',
progress: 0,
status: 'downloading',
message: `正在更新后端代码(分支: ${targetBranch})...`,
})
}
// 更新远程URL为镜像站URL避免直接访问GitHub
console.log(`📡 更新远程URL为镜像站: ${repoUrl}`)
await new Promise<void>((resolve, reject) => {
const proc = spawn(gitPath, ['remote', 'set-url', 'origin', repoUrl], {
stdio: 'pipe',
env: gitEnv,
cwd: backendPath,
})
proc.stdout?.on('data', d => console.log('git remote set-url stdout:', d.toString().trim()))
proc.stderr?.on('data', d => console.log('git remote set-url stderr:', d.toString().trim()))
proc.on('close', code => {
console.log(`git remote set-url 退出码: ${code}`)
if (code === 0) {
console.log('✅ 远程URL更新成功')
resolve()
} else {
console.error('❌ 远程URL更新失败')
reject(new Error(`git remote set-url失败退出码: ${code}`))
}
})
proc.on('error', error => {
console.error('❌ git remote set-url 进程错误:', error)
reject(error)
})
})
// 获取目标分支信息(显式 fetch 目标分支)
console.log(`📥 显式获取远程分支: ${targetBranch} ...`)
await new Promise<void>((resolve, reject) => {
const proc = spawn(gitPath, ['fetch', 'origin', targetBranch], {
stdio: 'pipe',
env: gitEnv,
cwd: backendPath,
})
proc.stdout?.on('data', d => console.log('git fetch stdout:', d.toString().trim()))
proc.stderr?.on('data', d => console.log('git fetch stderr:', d.toString().trim()))
proc.on('close', code => {
console.log(`git fetch origin ${targetBranch} 退出码: ${code}`)
if (code === 0) {
console.log(`✅ 成功获取远程分支: ${targetBranch}`)
resolve()
} else {
console.error(`❌ 获取远程分支失败: ${targetBranch}`)
reject(new Error(`git fetch origin ${targetBranch} 失败,退出码: ${code}`))
}
})
proc.on('error', error => {
console.error('❌ git fetch 进程错误:', error)
reject(error)
})
})
// 切换到目标分支
console.log(`🔀 切换到目标分支: ${targetBranch}`)
await new Promise<void>((resolve, reject) => {
const proc = spawn(gitPath, ['checkout', '-B', targetBranch, `origin/${targetBranch}`], {
stdio: 'pipe',
env: gitEnv,
cwd: backendPath,
})
proc.stdout?.on('data', d => console.log('git checkout stdout:', d.toString().trim()))
proc.stderr?.on('data', d => console.log('git checkout stderr:', d.toString().trim()))
proc.on('close', code => {
console.log(`git checkout 退出码: ${code}`)
if (code === 0) {
console.log(`✅ 成功切换到分支: ${targetBranch}`)
resolve()
} else {
console.error(`❌ 切换分支失败: ${targetBranch}`)
reject(new Error(`git checkout失败退出码: ${code}`))
}
})
proc.on('error', error => {
console.error('❌ git checkout 进程错误:', error)
reject(error)
})
})
// 执行pull操作
console.log('🔄 强制同步到远程分支最新提交...')
await new Promise<void>((resolve, reject) => {
const proc = spawn(gitPath, ['reset', '--hard', `origin/${targetBranch}`], {
stdio: 'pipe',
env: gitEnv,
cwd: backendPath,
})
proc.stdout?.on('data', d => console.log('git reset stdout:', d.toString().trim()))
proc.stderr?.on('data', d => console.log('git reset stderr:', d.toString().trim()))
proc.on('close', code => {
console.log(`git reset --hard 退出码: ${code}`)
if (code === 0) {
console.log('✅ 代码已强制更新到远程最新版本')
resolve()
} else {
console.error('❌ 代码重置失败')
reject(new Error(`git reset --hard 失败,退出码: ${code}`))
}
})
proc.on('error', error => {
console.error('❌ git reset 进程错误:', error)
reject(error)
})
})
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'backend',
progress: 100,
status: 'completed',
message: `后端代码更新完成(分支: ${targetBranch})`,
})
}
console.log(`✅ 后端代码更新完成(分支: ${targetBranch})`)
} else {
console.log('=== 克隆新的Git仓库 ===')
// 不是 git 仓库clone 到 tmp再拷贝出来
const tmpDir = path.join(appRoot, 'git_tmp')
console.log(`临时目录: ${tmpDir}`)
if (fs.existsSync(tmpDir)) {
console.log('🗑️ 清理现有临时目录...')
fs.rmSync(tmpDir, { recursive: true, force: true })
}
console.log('📁 创建临时目录...')
fs.mkdirSync(tmpDir, { recursive: true })
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'backend',
progress: 0,
status: 'downloading',
message: `正在克隆后端代码(分支: ${targetBranch})...`,
})
}
console.log(`📥 开始克隆代码到临时目录...`)
console.log(`克隆参数: --single-branch --depth 1 --branch ${targetBranch}`)
await new Promise<void>((resolve, reject) => {
const proc = spawn(
gitPath,
[
'clone',
'--progress',
'--verbose',
'--single-branch',
'--depth',
'1',
'--branch',
targetBranch,
repoUrl,
tmpDir,
],
{
stdio: 'pipe',
env: gitEnv,
cwd: appRoot,
}
)
proc.stdout?.on('data', d => console.log('git clone stdout:', d.toString().trim()))
proc.stderr?.on('data', d => console.log('git clone stderr:', d.toString().trim()))
proc.on('close', code => {
console.log(`git clone 退出码: ${code}`)
if (code === 0) {
console.log('✅ 代码克隆成功')
resolve()
} else {
console.error('❌ 代码克隆失败')
reject(new Error(`git clone失败退出码: ${code}`))
}
})
proc.on('error', error => {
console.error('❌ git clone 进程错误:', error)
reject(error)
})
})
// 复制所有文件到 backendPathappRoot包含 .git
console.log('📋 复制文件到目标目录...')
const tmpFiles = fs.readdirSync(tmpDir)
console.log(`临时目录中的文件: ${tmpFiles.join(', ')}`)
for (const file of tmpFiles) {
const src = path.join(tmpDir, file)
const dst = path.join(backendPath, file)
console.log(`复制: ${file}`)
if (fs.existsSync(dst)) {
console.log(` - 删除现有文件/目录: ${dst}`)
if (fs.statSync(dst).isDirectory()) fs.rmSync(dst, { recursive: true, force: true })
else fs.unlinkSync(dst)
}
if (fs.statSync(src).isDirectory()) {
console.log(` - 复制目录: ${src} -> ${dst}`)
copyDirSync(src, dst)
} else {
console.log(` - 复制文件: ${src} -> ${dst}`)
fs.copyFileSync(src, dst)
}
}
console.log('🗑️ 清理临时目录...')
fs.rmSync(tmpDir, { recursive: true, force: true })
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'backend',
progress: 100,
status: 'completed',
message: `后端代码克隆完成(分支: ${targetBranch})`,
})
}
console.log(`✅ 后端代码克隆完成(分支: ${targetBranch})`)
}
console.log('=== 后端代码获取操作完成 ===')
return { success: true }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error('❌ 获取后端代码失败:', errorMessage)
console.error('错误堆栈:', error instanceof Error ? error.stack : 'N/A')
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'backend',
progress: 0,
status: 'error',
message: `后端代码获取失败: ${errorMessage}`,
})
}
return { success: false, error: errorMessage }
}
}

View File

@@ -0,0 +1,168 @@
import log from 'electron-log'
import * as path from 'path'
import { getAppRoot } from './environmentService'
// 移除ANSI颜色转义字符的函数
function stripAnsiColors(text: string): string {
// 匹配ANSI转义序列的正则表达式 - 更完整的模式
const ansiRegex = /\x1b\[[0-9;]*[mGKHF]|\x1b\[[\d;]*[A-Za-z]/g
return text.replace(ansiRegex, '')
}
// 获取应用安装目录下的日志路径
function getLogDirectory(): string {
const appRoot = getAppRoot()
return path.join(appRoot, 'logs')
}
// 获取当前日期的日志文件名 - 使用ISO 8601格式
function getTodayLogFileName(): string {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
return `frontendlog-${year}-${month}-${day}.log`
}
// 配置日志系统
export function setupLogger() {
// 设置日志文件路径到软件安装目录
const logPath = getLogDirectory()
// 确保日志目录存在
const fs = require('fs')
if (!fs.existsSync(logPath)) {
fs.mkdirSync(logPath, { recursive: true })
}
// 配置日志格式
log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'
log.transports.console.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'
// 设置主进程日志文件路径和名称 - 按日期分文件
log.transports.file.resolvePathFn = () => {
const fileName = getTodayLogFileName()
return path.join(logPath, fileName)
}
// 设置日志级别
log.transports.file.level = 'debug'
log.transports.console.level = 'debug'
// 设置文件大小限制 (50MB因为按日期分文件可以设置更大)
log.transports.file.maxSize = 50 * 1024 * 1024
// 禁用自动归档,因为我们按日期分文件
log.transports.file.archiveLog = () => {
/* do nothing */
};
// 捕获未处理的异常和Promise拒绝
log.catchErrors({
showDialog: false,
onError: (options: any) => {
log.error('未处理的错误:', options.error)
log.error('版本信息:', options.versions)
log.error('进程类型:', options.processType)
},
})
// 重写console方法将所有控制台输出重定向到日志
const originalConsole = {
log: console.log,
error: console.error,
warn: console.warn,
info: console.info,
debug: console.debug,
}
console.log = (...args) => {
log.info(...args)
originalConsole.log(...args)
}
console.error = (...args) => {
log.error(...args)
originalConsole.error(...args)
}
console.warn = (...args) => {
log.warn(...args)
originalConsole.warn(...args)
}
console.info = (...args) => {
log.info(...args)
originalConsole.info(...args)
}
console.debug = (...args) => {
log.debug(...args)
originalConsole.debug(...args)
}
log.info('日志系统初始化完成')
log.info(`日志文件路径: ${path.join(logPath, getTodayLogFileName())}`)
return log
}
// 导出日志实例和工具函数
export { log, stripAnsiColors }
// 获取当前日志文件路径
export function getLogPath(): string {
return path.join(getLogDirectory(), getTodayLogFileName())
}
// 获取所有日志文件列表
export function getLogFiles(): string[] {
const fs = require('fs')
const logDir = getLogDirectory()
if (!fs.existsSync(logDir)) {
return []
}
const files = fs.readdirSync(logDir)
return files
.filter((file: string) => file.match(/^frontendlog-\d{4}-\d{2}-\d{2}\.log$/))
.sort()
.reverse() // 最新的在前面
}
// 清理旧日志文件
export function cleanOldLogs(daysToKeep: number = 7) {
const fs = require('fs')
const logDir = getLogDirectory()
if (!fs.existsSync(logDir)) {
return
}
const files = fs.readdirSync(logDir)
const now = new Date()
const cutoffDate = new Date(now.getTime() - daysToKeep * 24 * 60 * 60 * 1000)
// 格式化截止日期为YYYY-MM-DD
const cutoffDateStr = cutoffDate.getFullYear() + '-' +
String(cutoffDate.getMonth() + 1).padStart(2, '0') + '-' +
String(cutoffDate.getDate()).padStart(2, '0')
files.forEach((file: string) => {
// 匹配日志文件名格式 frontendlog-YYYY-MM-DD.log
const match = file.match(/^frontendlog-(\d{4}-\d{2}-\d{2})\.log$/)
if (match) {
const fileDateStr = match[1]
if (fileDateStr < cutoffDateStr) {
const filePath = path.join(logDir, file)
try {
fs.unlinkSync(filePath)
log.info(`已删除旧日志文件: ${file}`)
} catch (error) {
log.error(`删除旧日志文件失败: ${file}`, error)
}
}
}
})
}

View File

@@ -0,0 +1,642 @@
import * as path from 'path'
import * as fs from 'fs'
import { spawn } from 'child_process'
import { BrowserWindow } from 'electron'
import AdmZip from 'adm-zip'
import { downloadFile } from './downloadService'
import { ChildProcessWithoutNullStreams } from 'node:child_process'
import { log, stripAnsiColors } from './logService'
let mainWindow: BrowserWindow | null = null
export function setMainWindow(window: BrowserWindow) {
mainWindow = window
}
// Python镜像源URL映射
const pythonMirrorUrls = {
official: 'https://www.python.org/ftp/python/3.12.0/python-3.12.0-embed-amd64.zip',
tsinghua: 'https://mirrors.tuna.tsinghua.edu.cn/python/3.12.0/python-3.12.0-embed-amd64.zip',
ustc: 'https://mirrors.ustc.edu.cn/python/3.12.0/python-3.12.0-embed-amd64.zip',
huawei:
'https://mirrors.huaweicloud.com/repository/toolkit/python/3.12.0/python-3.12.0-embed-amd64.zip',
aliyun: 'https://mirrors.aliyun.com/python-release/windows/python-3.12.0-embed-amd64.zip',
}
// 检查pip是否已安装
function isPipInstalled(pythonPath: string): boolean {
const scriptsPath = path.join(pythonPath, 'Scripts')
const pipExePath = path.join(scriptsPath, 'pip.exe')
const pip3ExePath = path.join(scriptsPath, 'pip3.exe')
console.log(`检查pip安装状态:`)
console.log(`Scripts目录: ${scriptsPath}`)
console.log(`pip.exe路径: ${pipExePath}`)
console.log(`pip3.exe路径: ${pip3ExePath}`)
const scriptsExists = fs.existsSync(scriptsPath)
const pipExists = fs.existsSync(pipExePath)
const pip3Exists = fs.existsSync(pip3ExePath)
console.log(`Scripts目录存在: ${scriptsExists}`)
console.log(`pip.exe存在: ${pipExists}`)
console.log(`pip3.exe存在: ${pip3Exists}`)
return scriptsExists && (pipExists || pip3Exists)
}
// 安装pip
async function installPip(pythonPath: string, appRoot: string): Promise<void> {
console.log('开始检查pip安装状态...')
const pythonExe = path.join(pythonPath, 'python.exe')
// 检查Python可执行文件是否存在
if (!fs.existsSync(pythonExe)) {
throw new Error(`Python可执行文件不存在: ${pythonExe}`)
}
// 检查pip是否已安装
if (isPipInstalled(pythonPath)) {
console.log('pip已经安装跳过安装步骤')
console.log('检测到pip.exe文件存在认为pip安装成功')
console.log('pip检查完成')
return
}
console.log('pip未安装开始安装...')
const getPipPath = path.join(pythonPath, 'get-pip.py')
const getPipUrl = 'https://download.auto-mas.top/d/AUTO_MAS/get-pip.py'
console.log(`Python可执行文件路径: ${pythonExe}`)
console.log(`get-pip.py下载URL: ${getPipUrl}`)
console.log(`get-pip.py保存路径: ${getPipPath}`)
// 下载get-pip.py
console.log('开始下载get-pip.py...')
try {
await downloadFile(getPipUrl, getPipPath)
console.log('get-pip.py下载完成')
// 检查下载的文件大小
const stats = fs.statSync(getPipPath)
console.log(`get-pip.py文件大小: ${stats.size} bytes`)
if (stats.size < 10000) {
// 如果文件小于10KB可能是无效文件
throw new Error(`get-pip.py文件大小异常: ${stats.size} bytes可能下载失败`)
}
} catch (error) {
console.error('下载get-pip.py失败:', error)
throw new Error(`下载get-pip.py失败: ${error}`)
}
// 执行pip安装
await new Promise<void>((resolve, reject) => {
console.log('执行pip安装命令...')
const process = spawn(pythonExe, [getPipPath], {
cwd: pythonPath,
stdio: 'pipe',
})
process.stdout?.on('data', data => {
const output = stripAnsiColors(data.toString())
log.info('pip安装输出:', output)
})
process.stderr?.on('data', data => {
const errorOutput = stripAnsiColors(data.toString())
log.warn('pip安装错误输出:', errorOutput)
})
process.on('close', code => {
console.log(`pip安装完成退出码: ${code}`)
if (code === 0) {
console.log('pip安装成功')
resolve()
} else {
reject(new Error(`pip安装失败退出码: ${code}`))
}
})
process.on('error', error => {
console.error('pip安装进程错误:', error)
reject(error)
})
})
// 验证pip是否安装成功
console.log('验证pip安装...')
await new Promise<void>((resolve, reject) => {
const verifyProcess = spawn(pythonExe, ['-m', 'pip', '--version'], {
cwd: pythonPath,
stdio: 'pipe',
})
verifyProcess.stdout?.on('data', data => {
const output = stripAnsiColors(data.toString())
log.info('pip版本信息:', output)
})
verifyProcess.stderr?.on('data', data => {
const errorOutput = stripAnsiColors(data.toString())
log.warn('pip版本检查错误:', errorOutput)
})
verifyProcess.on('close', code => {
if (code === 0) {
console.log('pip验证成功')
resolve()
} else {
reject(new Error(`pip验证失败退出码: ${code}`))
}
})
verifyProcess.on('error', error => {
console.error('pip验证进程错误:', error)
reject(error)
})
})
// 清理临时文件
console.log('清理临时文件...')
try {
if (fs.existsSync(getPipPath)) {
fs.unlinkSync(getPipPath)
console.log('get-pip.py临时文件已删除')
}
} catch (error) {
console.warn('清理get-pip.py文件时出错:', error)
}
console.log('pip安装和验证完成')
}
// 下载Python
export async function downloadPython(
appRoot: string,
mirror = 'ustc'
): Promise<{ success: boolean; error?: string }> {
try {
const environmentPath = path.join(appRoot, 'environment')
const pythonPath = path.join(environmentPath, 'python')
// 确保environment目录存在
if (!fs.existsSync(environmentPath)) {
fs.mkdirSync(environmentPath, { recursive: true })
}
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'python',
progress: 0,
status: 'downloading',
message: '开始下载Python...',
})
}
// 根据选择的镜像源获取下载链接
const pythonUrl =
pythonMirrorUrls[mirror as keyof typeof pythonMirrorUrls] || pythonMirrorUrls.ustc
const zipPath = path.join(environmentPath, 'python.zip')
await downloadFile(pythonUrl, zipPath)
// 检查下载的Python文件大小
const stats = fs.statSync(zipPath)
console.log(
`Python压缩包大小: ${stats.size} bytes (${(stats.size / 1024 / 1024).toFixed(2)} MB)`
)
// Python 3.12.0嵌入式版本应该大约30MB如果小于5MB可能是无效文件
if (stats.size < 5 * 1024 * 1024) {
// 5MB
fs.unlinkSync(zipPath) // 删除无效文件
throw new Error(
`Python下载文件大小异常: ${stats.size} bytes (${(stats.size / 1024).toFixed(2)} KB)。可能是对应镜像站不可用。请选择任意一个其他镜像源进行下载!`
)
}
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'python',
progress: 100,
status: 'extracting',
message: '正在解压Python...',
})
}
// 解压Python到指定目录
console.log(`开始解压Python到: ${pythonPath}`)
// 确保Python目录存在
if (!fs.existsSync(pythonPath)) {
fs.mkdirSync(pythonPath, { recursive: true })
console.log(`创建Python目录: ${pythonPath}`)
}
const zip = new AdmZip(zipPath)
zip.extractAllTo(pythonPath, true)
console.log(`Python解压完成到: ${pythonPath}`)
// 删除zip文件
fs.unlinkSync(zipPath)
console.log(`删除临时文件: ${zipPath}`)
// 启用 site-packages 支持
const pthFile = path.join(pythonPath, 'python312._pth')
if (fs.existsSync(pthFile)) {
let content = fs.readFileSync(pthFile, 'utf-8')
content = content.replace(/^#import site/m, 'import site')
fs.writeFileSync(pthFile, content, 'utf-8')
console.log('已启用 site-packages 支持')
}
// 安装pip
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'python',
progress: 80,
status: 'installing',
message: '正在安装pip...',
})
}
await installPip(pythonPath, appRoot)
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'python',
progress: 100,
status: 'completed',
message: 'Python和pip安装完成',
})
}
return { success: true }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'python',
progress: 0,
status: 'error',
message: `Python下载失败: ${errorMessage}`,
})
}
return { success: false, error: errorMessage }
}
}
// pip镜像源URL映射
const pipMirrorUrls = {
official: 'https://pypi.org/simple/',
tsinghua: 'https://pypi.tuna.tsinghua.edu.cn/simple/',
ustc: 'https://pypi.mirrors.ustc.edu.cn/simple/',
aliyun: 'https://mirrors.aliyun.com/pypi/simple/',
douban: 'https://pypi.douban.com/simple/',
}
// 安装Python依赖
export async function installDependencies(
appRoot: string,
mirror = 'tsinghua'
): Promise<{
success: boolean
error?: string
}> {
try {
const pythonPath = path.join(appRoot, 'environment', 'python', 'python.exe')
const backendPath = path.join(appRoot)
const requirementsPath = path.join(appRoot, 'requirements.txt')
// 检查文件是否存在
if (!fs.existsSync(pythonPath)) {
throw new Error('Python可执行文件不存在')
}
if (!fs.existsSync(requirementsPath)) {
throw new Error('requirements.txt文件不存在')
}
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'dependencies',
progress: 0,
status: 'downloading',
message: '正在安装Python依赖包...',
})
}
// 获取pip镜像源URL
const pipMirrorUrl =
pipMirrorUrls[mirror as keyof typeof pipMirrorUrls] || pipMirrorUrls.tsinghua
// 使用Scripts文件夹中的pip.exe
const pythonDir = path.join(appRoot, 'environment', 'python')
const pipExePath = path.join(pythonDir, 'Scripts', 'pip.exe')
console.log(`开始安装Python依赖`)
console.log(`Python目录: ${pythonDir}`)
console.log(`pip.exe路径: ${pipExePath}`)
console.log(`requirements.txt路径: ${requirementsPath}`)
console.log(`pip镜像源: ${pipMirrorUrl}`)
// 检查pip.exe是否存在
if (!fs.existsSync(pipExePath)) {
throw new Error(`pip.exe不存在: ${pipExePath}`)
}
// 安装依赖 - 直接使用pip.exe而不是python -m pip
await new Promise<void>((resolve, reject) => {
const process = spawn(
pipExePath,
[
'install',
'-r',
requirementsPath,
'-i',
pipMirrorUrl,
'--trusted-host',
new URL(pipMirrorUrl).hostname,
],
{
cwd: backendPath,
stdio: 'pipe',
}
)
process.stdout?.on('data', data => {
const output = stripAnsiColors(data.toString())
log.info('Pip output:', output)
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'dependencies',
progress: 50,
status: 'downloading',
message: '正在安装依赖包...',
})
}
})
process.stderr?.on('data', data => {
const errorOutput = stripAnsiColors(data.toString())
log.error('Pip error:', errorOutput)
})
process.on('close', code => {
console.log(`pip安装完成退出码: ${code}`)
if (code === 0) {
resolve()
} else {
reject(new Error(`依赖安装失败,退出码: ${code}`))
}
})
process.on('error', error => {
console.error('pip进程错误:', error)
reject(error)
})
})
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'dependencies',
progress: 100,
status: 'completed',
message: 'Python依赖安装完成',
})
}
return { success: true }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'dependencies',
progress: 0,
status: 'error',
message: `依赖安装失败: ${errorMessage}`,
})
}
return { success: false, error: errorMessage }
}
}
// 导出pip安装函数
export async function installPipPackage(
appRoot: string
): Promise<{ success: boolean; error?: string }> {
try {
const pythonPath = path.join(appRoot, 'environment', 'python')
if (!fs.existsSync(pythonPath)) {
throw new Error('Python环境不存在请先安装Python')
}
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'pip',
progress: 0,
status: 'installing',
message: '正在安装pip...',
})
}
await installPip(pythonPath, appRoot)
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'pip',
progress: 100,
status: 'completed',
message: 'pip安装完成',
})
}
return { success: true }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'pip',
progress: 0,
status: 'error',
message: `pip安装失败: ${errorMessage}`,
})
}
return { success: false, error: errorMessage }
}
}
// 启动后端
let backendProc: ChildProcessWithoutNullStreams | null = null
/**
* 启动后端
* @param appRoot 项目根目录
* @param timeoutMs 等待启动超时(默认 30 秒)
*/
export async function startBackend(appRoot: string, timeoutMs = 30_000) {
try {
// 如果已经在运行,直接返回
if (backendProc && !backendProc.killed && backendProc.exitCode == null) {
console.log('[Backend] 已在运行, PID =', backendProc.pid)
return { success: true }
}
const pythonExe = path.join(appRoot, 'environment', 'python', 'python.exe')
const mainPy = path.join(appRoot, 'main.py')
if (!fs.existsSync(pythonExe)) {
throw new Error(`Python可执行文件不存在: ${pythonExe}`)
}
if (!fs.existsSync(mainPy)) {
throw new Error(`后端主文件不存在: ${mainPy}`)
}
console.log(`[Backend] spawn "${pythonExe}" "${mainPy}" (cwd=${appRoot})`)
backendProc = spawn(pythonExe, [mainPy], {
cwd: appRoot,
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
})
backendProc.stdout.setEncoding('utf8')
backendProc.stderr.setEncoding('utf8')
backendProc.stdout.on('data', d => {
const line = stripAnsiColors(d.toString().trim())
if (line) log.info('[Backend]', line)
})
backendProc.stderr.on('data', d => {
const line = stripAnsiColors(d.toString().trim())
if (line) log.info('[Backend]', line)
})
backendProc.once('exit', (code, signal) => {
console.log('[Backend] 退出', { code, signal })
backendProc = null
})
backendProc.once('error', e => {
console.error('[Backend] 进程错误:', e)
})
// 等待启动成功(匹配 Uvicorn 的输出)
await new Promise<void>((resolve, reject) => {
let settled = false
const timer = setTimeout(() => {
if (!settled) {
settled = true
reject(new Error('后端启动超时'))
}
}, timeoutMs)
const checkReady = (buf: Buffer | string) => {
if (settled) return
const s = buf.toString()
if (/Uvicorn running|http:\/\/0\.0\.0\.0:\d+/.test(s)) {
settled = true
clearTimeout(timer)
resolve()
}
}
backendProc!.stdout.on('data', checkReady)
backendProc!.stderr.on('data', checkReady)
backendProc!.once('exit', (code, sig) => {
if (!settled) {
settled = true
clearTimeout(timer)
reject(new Error(`后端提前退出: code=${code}, signal=${sig ?? ''}`))
}
})
backendProc!.once('error', err => {
if (!settled) {
settled = true
clearTimeout(timer)
reject(err)
}
})
})
console.log('[Backend] 启动成功, PID =', backendProc.pid)
return { success: true }
} catch (e) {
console.error('[Backend] 启动失败:', e)
return { success: false, error: e instanceof Error ? e.message : String(e) }
}
}
/** 停止后端进程(如果没启动就直接返回成功) */
export async function stopBackend() {
if (!backendProc || backendProc.killed) {
console.log('[Backend] 未运行,无需停止')
return { success: true }
}
const pid = backendProc.pid
console.log('[Backend] 正在停止后端服务, PID =', pid)
return new Promise<{ success: boolean; error?: string }>(resolve => {
// 设置超时,确保不会无限等待
const timeout = setTimeout(() => {
console.warn('[Backend] 停止超时,强制结束进程')
try {
if (backendProc && !backendProc.killed) {
// 在 Windows 上使用 taskkill 强制结束进程树
if (process.platform === 'win32') {
const { exec } = require('child_process')
exec(`taskkill /f /t /pid ${pid}`, (error: any) => {
if (error) {
console.error('[Backend] taskkill 失败:', error)
} else {
console.log('[Backend] 进程树已强制结束')
}
})
} else {
backendProc.kill('SIGKILL')
}
}
} catch (e) {
console.error('[Backend] 强制结束失败:', e)
}
backendProc = null
resolve({ success: true })
}, 2000) // 2秒超时
// 清监听,避免重复日志
backendProc?.stdout?.removeAllListeners('data')
backendProc?.stderr?.removeAllListeners('data')
backendProc!.once('exit', (code, signal) => {
clearTimeout(timeout)
console.log('[Backend] 已退出', { code, signal })
backendProc = null
resolve({ success: true })
})
backendProc!.once('error', err => {
clearTimeout(timeout)
console.error('[Backend] 停止时出错:', err)
backendProc = null
resolve({ success: false, error: err instanceof Error ? err.message : String(err) })
})
try {
// 首先尝试优雅关闭
backendProc!.kill('SIGTERM')
console.log('[Backend] 已发送 SIGTERM 信号')
} catch (e) {
clearTimeout(timeout)
console.error('[Backend] kill 调用失败:', e)
backendProc = null
resolve({ success: false, error: e instanceof Error ? e.message : String(e) })
}
})
}

View File

@@ -0,0 +1,125 @@
import { exec } from 'child_process'
import * as path from 'path'
import { getAppRoot } from '../services/environmentService'
export interface ProcessInfo {
pid: number
name: string
commandLine: string
}
/**
* 获取所有相关的进程信息
*/
export async function getRelatedProcesses(): Promise<ProcessInfo[]> {
return new Promise((resolve) => {
if (process.platform !== 'win32') {
resolve([])
return
}
const appRoot = getAppRoot()
const pythonExePath = path.join(appRoot, 'environment', 'python', 'python.exe')
// 使用 wmic 获取详细的进程信息
const cmd = `wmic process where "Name='python.exe' or Name='AUTO-MAS.exe' or CommandLine like '%main.py%'" get ProcessId,Name,CommandLine /format:csv`
exec(cmd, (error, stdout, stderr) => {
if (error) {
console.error('获取进程信息失败:', error)
resolve([])
return
}
const processes: ProcessInfo[] = []
const lines = stdout.split('\n').filter(line => line.trim() && !line.startsWith('Node'))
for (const line of lines) {
const parts = line.split(',')
if (parts.length >= 4) {
const commandLine = parts[1] || ''
const name = parts[2] || ''
const pid = parseInt(parts[3]) || 0
if (pid > 0 && (
commandLine.includes(pythonExePath) ||
commandLine.includes('main.py') ||
name === 'AUTO-MAS.exe'
)) {
processes.push({ pid, name, commandLine })
}
}
}
resolve(processes)
})
})
}
/**
* 强制结束指定的进程
*/
export async function killProcess(pid: number): Promise<boolean> {
return new Promise((resolve) => {
if (process.platform !== 'win32') {
resolve(false)
return
}
exec(`taskkill /f /t /pid ${pid}`, (error) => {
if (error) {
console.error(`结束进程 ${pid} 失败:`, error.message)
resolve(false)
} else {
console.log(`进程 ${pid} 已结束`)
resolve(true)
}
})
})
}
/**
* 强制结束所有相关进程
*/
export async function killAllRelatedProcesses(): Promise<void> {
console.log('开始清理所有相关进程...')
const processes = await getRelatedProcesses()
console.log(`找到 ${processes.length} 个相关进程:`)
for (const proc of processes) {
console.log(`- PID: ${proc.pid}, Name: ${proc.name}, CMD: ${proc.commandLine.substring(0, 100)}...`)
}
// 并行结束所有进程
const killPromises = processes.map(proc => killProcess(proc.pid))
await Promise.all(killPromises)
console.log('进程清理完成')
}
/**
* 等待进程结束
*/
export async function waitForProcessExit(pid: number, timeoutMs: number = 5000): Promise<boolean> {
return new Promise((resolve) => {
const startTime = Date.now()
const checkProcess = () => {
if (Date.now() - startTime > timeoutMs) {
resolve(false)
return
}
exec(`tasklist /fi "PID eq ${pid}"`, (error, stdout) => {
if (error || !stdout.includes(pid.toString())) {
resolve(true)
} else {
setTimeout(checkProcess, 100)
}
})
}
checkProcess()
})
}

34
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,34 @@
const vue = require('eslint-plugin-vue');
const ts = require('@typescript-eslint/eslint-plugin');
const tsParser = require('@typescript-eslint/parser');
const prettier = require('eslint-plugin-prettier');
module.exports = [
// 推荐的 vue3 配置
vue.configs['vue3-recommended'],
// 推荐的 ts 配置
ts.configs.recommended,
// 推荐的 prettier 配置
prettier.configs.recommended,
// 自定义规则和文件范围
{
files: ['**/*.js', '**/*.ts', '**/*.vue'],
ignores: ['dist/**', 'node_modules/**'],
languageOptions: {
parser: tsParser,
ecmaVersion: 2021,
sourceType: 'module',
},
plugins: {
vue,
'@typescript-eslint': ts,
prettier,
},
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off',
// 如果你希望 prettier 报错,取消注释下面一行
// 'prettier/prettier': 'error',
},
},
];

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AUTO-MAS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

116
frontend/package.json Normal file
View File

@@ -0,0 +1,116 @@
{
"name": "frontend",
"private": true,
"version": "v5.0.0-alpha.3",
"main": "dist-electron/main.js",
"scripts": {
"dev": "concurrently \"vite\" \"yarn watch:main\" \"yarn electron-dev\"",
"watch:main": "tsc -p tsconfig.electron.json --watch",
"electron-dev": "wait-on http://localhost:5173 && cross-env VITE_DEV_SERVER_URL=http://localhost:5173 electron .",
"build:main": "tsc -p tsconfig.electron.json",
"build": "vite build && yarn build:main && electron-builder",
"web": "vite",
"release": "vite build && yarn build:main && electron-builder --win --publish always"
},
"build": {
"asar": true,
"asarUnpack": [],
"extraMetadata": {
"env": "prod"
},
"appId": "top.auto-mas.frontend",
"productName": "AUTO-MAS",
"files": [
"dist/**",
"dist-electron/**",
"public/**",
"!src/**",
"!**/*.map"
],
"publish": [
{
"provider": "github",
"owner": "AUTO-MAS-Project",
"repo": "AUTO-MAS"
}
],
"extraResources": [
{
"from": "src/assets",
"to": "assets",
"filter": [
"**/*"
]
}
],
"win": {
"requestedExecutionLevel": "requireAdministrator",
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
},
{
"target": "zip",
"arch": [
"x64"
]
}
],
"icon": "public/AUTO-MAS.ico",
"artifactName": "AUTO-MAS-Setup-${version}-${arch}.${ext}"
},
"nsis": {
"oneClick": false,
"perMachine": true,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"shortcutName": "AUTO-MAS",
"differentialPackage": true
}
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@types/adm-zip": "^0.5.7",
"@types/markdown-it": "^14.1.2",
"adm-zip": "^0.5.16",
"ant-design-vue": "4.x",
"axios": "^1.11.0",
"dayjs": "^1.11.13",
"electron-log": "^5.4.3",
"form-data": "^4.0.4",
"markdown-it": "^14.1.0",
"vue": "^3.5.17",
"vue-router": "4",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@types/node": "22.17.1",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.7.0",
"concurrently": "^9.2.0",
"cross-env": "^10.0.0",
"electron": "^37.2.5",
"electron-builder": "^26.0.12",
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.3",
"eslint-plugin-vue": "^10.4.0",
"openapi-typescript-codegen": "^0.29.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2",
"vite": "^7.0.4",
"vite-plugin-eslint": "^1.8.1",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^2.2.12",
"wait-on": "^8.0.4"
},
"resolutions": {
"@types/node": "22.17.1"
},
"packageManager": "yarn@4.9.1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

509
frontend/public/dialog.html Normal file
View File

@@ -0,0 +1,509 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>操作确认</title>
<style>
:root {
/* 亮色模式变量 */
--primary-color: #1677ff;
--primary-hover: #4096ff;
--primary-active: #0958d9;
--danger-color: #ff4d4f;
--danger-hover: #ff7875;
--danger-active: #d9363e;
--success-color: #52c41a;
--warning-color: #faad14;
--text-primary: #262626;
--text-secondary: #595959;
--text-tertiary: #8c8c8c;
--text-disabled: #bfbfbf;
--bg-primary: #ffffff;
--bg-secondary: #fafafa;
--bg-tertiary: #f5f5f5;
--bg-quaternary: #f0f0f0;
--border-primary: #d9d9d9;
--border-secondary: #f0f0f0;
--border-hover: #4096ff;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
/* 暗色模式变量 */
[data-theme="dark"] {
--primary-color: #1668dc;
--primary-hover: #3c89e8;
--primary-active: #1554ad;
--danger-color: #ff4d4f;
--danger-hover: #ff7875;
--danger-active: #d9363e;
--text-primary: #ffffff;
--text-secondary: #c9cdd4;
--text-tertiary: #a6adb4;
--text-disabled: #6c757d;
--bg-primary: #1f1f1f;
--bg-secondary: #2a2a2a;
--bg-tertiary: #373737;
--bg-quaternary: #404040;
--border-primary: #434343;
--border-secondary: #303030;
--border-hover: #3c89e8;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.15), 0 1px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px 0 rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -1px rgba(0, 0, 0, 0.15);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.25), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family);
font-size: 14px;
line-height: 1.5715;
color: var(--text-primary);
background: var(--bg-primary);
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
transition: color 0.2s ease, background-color 0.2s ease;
user-select: none;
display: flex;
flex-direction: column;
}
.dialog-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: var(--bg-primary);
border-radius: var(--radius-md);
border: 1px solid var(--border-primary);
transition: background-color 0.2s ease, border-color 0.2s ease;
box-shadow: var(--shadow-lg);
}
.dialog-header {
padding: 12px 16px 8px 16px;
background: var(--bg-primary);
border-radius: var(--radius-md) var(--radius-md) 0 0;
transition: background-color 0.2s ease;
cursor: move;
-webkit-app-region: drag;
border-bottom: 1px solid var(--border-secondary);
}
.dialog-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
text-align: center;
transition: color 0.2s ease;
}
.dialog-content {
padding: 16px;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-primary);
transition: background-color 0.2s ease;
}
.dialog-message {
font-size: 13px;
line-height: 1.5;
color: var(--text-secondary);
margin: 0;
word-wrap: break-word;
white-space: pre-wrap;
text-align: center;
transition: color 0.2s ease;
max-width: 100%;
}
.dialog-actions {
padding: 12px 16px 12px 16px;
display: flex;
justify-content: center;
gap: 8px;
background: var(--bg-primary);
border-radius: 0 0 var(--radius-md) var(--radius-md);
transition: background-color 0.2s ease;
}
.dialog-button {
padding: 6px 12px;
height: 28px;
border: 1px solid var(--border-primary);
border-radius: var(--radius-sm);
background: var(--bg-primary);
color: var(--text-primary);
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
min-width: 60px;
outline: none;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
font-family: var(--font-family);
line-height: 1.5715;
}
.dialog-button:hover {
background: var(--bg-quaternary);
border-color: var(--border-hover);
color: var(--primary-color);
}
.dialog-button:focus {
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.2);
border-color: var(--border-hover);
outline: none;
}
.dialog-button:active {
transform: translateY(0);
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.12);
}
.dialog-button.primary {
background: var(--primary-color);
color: #fff;
border-color: var(--primary-color);
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);
}
.dialog-button.primary:hover {
background: var(--primary-hover);
border-color: var(--primary-hover);
color: #fff;
}
.dialog-button.primary:active {
background: var(--primary-active);
border-color: var(--primary-active);
}
.dialog-button.danger {
background: var(--danger-color);
color: #fff;
border-color: var(--danger-color);
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);
}
.dialog-button.danger:hover {
background: var(--danger-hover);
border-color: var(--danger-hover);
color: #fff;
}
.dialog-button.danger:active {
background: var(--danger-active);
border-color: var(--danger-active);
}
/* 键盘导航样式 */
.dialog-button:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
/* 动画效果 */
.dialog-container {
animation: dialogFadeIn 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
@keyframes dialogFadeIn {
from {
opacity: 0;
transform: scale(0.98);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* 响应式设计 */
@media (max-width: 350px) {
.dialog-header {
padding: 8px 12px 6px 12px;
}
.dialog-title {
font-size: 12px;
}
.dialog-content {
padding: 12px;
}
.dialog-message {
font-size: 11px;
}
.dialog-actions {
padding: 8px 12px 8px 12px;
flex-direction: column;
gap: 6px;
}
.dialog-button {
width: 100%;
margin: 0;
font-size: 11px;
height: 24px;
}
}
</style>
</head>
<body>
<div class="dialog-container" role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describedby="dialog-message">
<div class="dialog-header">
<h3 class="dialog-title" id="dialog-title">操作确认</h3>
</div>
<div class="dialog-content">
<p class="dialog-message" id="dialog-message">是否要执行此操作?</p>
</div>
<div class="dialog-actions" id="dialog-actions" role="group" aria-label="对话框操作按钮">
<!-- 按钮将通过 JavaScript 动态生成 -->
</div>
</div>
<script>
// 主题管理
const ThemeManager = {
init() {
this.applyTheme();
this.listenForThemeChanges();
},
applyTheme() {
// 优先从 Electron 主进程获取软件内主题状态
if (window.electronAPI && window.electronAPI.getTheme) {
window.electronAPI.getTheme().then(theme => {
document.documentElement.setAttribute('data-theme', theme);
}).catch(() => {
// 如果获取失败,使用系统主题
this.useSystemTheme();
});
} else {
this.useSystemTheme();
}
},
useSystemTheme() {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
},
listenForThemeChanges() {
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
// 如果软件主题配置为系统跟随,则更新主题
if (!window.electronAPI || !window.electronAPI.getTheme) {
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
} else {
// 重新获取软件内主题状态
this.applyTheme();
}
});
// 监听 Electron 主题变化
if (window.electronAPI && window.electronAPI.onThemeChanged) {
window.electronAPI.onThemeChanged((theme) => {
document.documentElement.setAttribute('data-theme', theme);
});
}
}
};
// 拖拽管理
const DragManager = {
isDragging: false,
startX: 0,
startY: 0,
init() {
const header = document.querySelector('.dialog-header');
if (!header) return;
header.addEventListener('mousedown', this.handleMouseDown.bind(this));
document.addEventListener('mousemove', this.handleMouseMove.bind(this));
document.addEventListener('mouseup', this.handleMouseUp.bind(this));
// 防止文本选择
header.addEventListener('selectstart', (e) => e.preventDefault());
},
handleMouseDown(e) {
// 只有在头部区域才能拖拽
if (!e.target.closest('.dialog-header')) return;
this.isDragging = true;
this.startX = e.clientX;
this.startY = e.clientY;
const container = document.querySelector('.dialog-container');
container.style.transition = 'none';
e.preventDefault();
},
handleMouseMove(e) {
if (!this.isDragging) return;
const deltaX = e.clientX - this.startX;
const deltaY = e.clientY - this.startY;
// 通知 Electron 移动窗口
if (window.electronAPI && window.electronAPI.moveWindow) {
window.electronAPI.moveWindow(deltaX, deltaY);
}
e.preventDefault();
},
handleMouseUp(e) {
if (!this.isDragging) return;
this.isDragging = false;
const container = document.querySelector('.dialog-container');
container.style.transition = '';
e.preventDefault();
}
};
// 窗口加载完成后的初始化
window.addEventListener('load', () => {
// 初始化主题管理
ThemeManager.init();
});
// 获取传递的参数
const urlParams = new URLSearchParams(window.location.search);
const data = JSON.parse(decodeURIComponent(urlParams.get('data') || '{}'));
// 设置对话框内容
document.getElementById('dialog-title').textContent = data.title || '操作确认';
document.getElementById('dialog-message').textContent = data.message || '是否要执行此操作?';
// 创建按钮
const actionsContainer = document.getElementById('dialog-actions');
const options = data.options || ['确定', '取消'];
options.forEach((option, index) => {
const button = document.createElement('button');
button.className = 'dialog-button';
button.textContent = option;
// 根据按钮文本设置样式
if (option.includes('确定') || option.includes('是') || option.includes('同意')) {
button.className += ' primary';
} else if (option.includes('删除') || option.includes('危险')) {
button.className += ' danger';
}
// 绑定点击事件
button.addEventListener('click', () => {
// 添加点击动画
button.style.transform = 'scale(0.98)';
setTimeout(() => {
button.style.transform = '';
}, 100);
// 发送结果到主进程
if (window.electronAPI && window.electronAPI.dialogResponse) {
const choice = index === 0; // 第一个选项为 true
window.electronAPI.dialogResponse(data.messageId, choice);
}
});
actionsContainer.appendChild(button);
});
// 自动聚焦第一个按钮
setTimeout(() => {
const firstButton = actionsContainer.querySelector('.dialog-button');
if (firstButton) {
firstButton.focus();
}
}, 100);
// 键盘事件处理
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
// ESC 键相当于取消
if (window.electronAPI && window.electronAPI.dialogResponse) {
window.electronAPI.dialogResponse(data.messageId, false);
}
} else if (event.key === 'Enter') {
// Enter 键相当于确定
const focusedButton = document.activeElement;
if (focusedButton && focusedButton.classList.contains('dialog-button')) {
focusedButton.click();
} else {
// 如果没有聚焦按钮,默认点击第一个
const firstButton = actionsContainer.querySelector('.dialog-button');
if (firstButton) {
firstButton.click();
}
}
} else if (event.key === 'Tab') {
// Tab 键在按钮间切换
const buttons = Array.from(actionsContainer.querySelectorAll('.dialog-button'));
const currentIndex = buttons.indexOf(document.activeElement);
if (event.shiftKey) {
// Shift+Tab 向前切换
const prevIndex = currentIndex <= 0 ? buttons.length - 1 : currentIndex - 1;
buttons[prevIndex].focus();
} else {
// Tab 向后切换
const nextIndex = currentIndex >= buttons.length - 1 ? 0 : currentIndex + 1;
buttons[nextIndex].focus();
}
event.preventDefault();
}
});
// 窗口加载完成后的初始化
window.addEventListener('load', () => {
// 初始化主题管理
ThemeManager.init();
// 初始化拖拽管理
DragManager.init();
});
</script>
</body>
</html>

98
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,98 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ConfigProvider } from 'ant-design-vue'
import { useTheme } from './composables/useTheme.ts'
import { useUpdateModal } from './composables/useUpdateChecker.ts'
import AppLayout from './components/AppLayout.vue'
import TitleBar from './components/TitleBar.vue'
import UpdateModal from './components/UpdateModal.vue'
import DevDebugPanel from './components/DevDebugPanel.vue'
import GlobalPowerCountdown from './components/GlobalPowerCountdown.vue'
import WebSocketMessageListener from './components/WebSocketMessageListener.vue'
import WebSocketDebugPanel from './components/WebSocketDebugPanel.vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
import { logger } from '@/utils/logger'
const route = useRoute()
const { antdTheme, initTheme } = useTheme()
const { updateVisible, updateData, onUpdateConfirmed } = useUpdateModal()
// 判断是否为初始化页面
const isInitializationPage = computed(() => route.name === 'Initialization')
onMounted(() => {
logger.info('App组件已挂载')
initTheme()
logger.info('主题初始化完成')
})
</script>
<template>
<ConfigProvider :theme="antdTheme" :locale="zhCN">
<!-- 初始化页面使用带标题栏的全屏布局 -->
<div v-if="isInitializationPage" class="initialization-container">
<TitleBar />
<div class="initialization-content">
<router-view />
</div>
</div>
<!-- 其他页面使用带标题栏的应用布局 -->
<div v-else class="app-container">
<TitleBar />
<AppLayout />
</div>
<!-- 全局更新模态框 -->
<UpdateModal
v-model:visible="updateVisible"
:update-data="updateData"
@confirmed="onUpdateConfirmed"
/>
<!-- 开发环境调试面板 -->
<DevDebugPanel />
<!-- 全局电源倒计时弹窗 -->
<GlobalPowerCountdown />
<!-- WebSocket 消息监听组件 -->
<WebSocketMessageListener />
<!-- WebSocket 调试面板 (仅开发环境) -->
<WebSocketDebugPanel v-if="$route.query.debug === 'true'" />
</ConfigProvider>
</template>
<style>
* {
box-sizing: border-box;
}
.app-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.initialization-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.initialization-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
width: 100%;
/* 隐藏滚动条但保留滚动功能 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
/* 隐藏 Webkit 浏览器的滚动条 */
.initialization-content::-webkit-scrollbar {
display: none;
}
</style>

View File

@@ -0,0 +1,25 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: any;
public readonly request: ApiRequestOptions;
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
super(message);
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}

View File

@@ -0,0 +1,17 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ApiRequestOptions = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, any>;
readonly cookies?: Record<string, any>;
readonly headers?: Record<string, any>;
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
};

View File

@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ApiResult = {
readonly url: string;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly body: any;
};

View File

@@ -0,0 +1,131 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = 'CancelError';
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
#isResolved: boolean;
#isRejected: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
#reject?: (reason?: any) => void;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: OnCancel
) => void
) {
this.#isResolved = false;
this.#isRejected = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isResolved = true;
if (this.#resolve) this.#resolve(value);
};
const onReject = (reason?: any): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isRejected = true;
if (this.#reject) this.#reject(reason);
};
const onCancel = (cancelHandler: () => void): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, 'isResolved', {
get: (): boolean => this.#isResolved,
});
Object.defineProperty(onCancel, 'isRejected', {
get: (): boolean => this.#isRejected,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this.#isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
get [Symbol.toStringTag]() {
return "Cancellable Promise";
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this.#promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult> {
return this.#promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.#promise.finally(onFinally);
}
public cancel(): void {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this.#cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn('Cancellation threw an error', error);
return;
}
}
this.#cancelHandlers.length = 0;
if (this.#reject) this.#reject(new CancelError('Request aborted'));
}
public get isCancelled(): boolean {
return this.#isCancelled;
}
}

View File

@@ -0,0 +1,32 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
type Headers = Record<string, string>;
export type OpenAPIConfig = {
BASE: string;
VERSION: string;
WITH_CREDENTIALS: boolean;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
TOKEN?: string | Resolver<string> | undefined;
USERNAME?: string | Resolver<string> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
ENCODE_PATH?: ((path: string) => string) | undefined;
};
export const OpenAPI: OpenAPIConfig = {
BASE: '',
VERSION: '1.0.0',
WITH_CREDENTIALS: false,
CREDENTIALS: 'include',
TOKEN: undefined,
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};

View File

@@ -0,0 +1,323 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import axios from 'axios';
import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios';
import FormData from 'form-data';
import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import { CancelablePromise } from './CancelablePromise';
import type { OnCancel } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';
export const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => {
return value !== undefined && value !== null;
};
export const isString = (value: any): value is string => {
return typeof value === 'string';
};
export const isStringWithValue = (value: any): value is string => {
return isString(value) && value !== '';
};
export const isBlob = (value: any): value is Blob => {
return (
typeof value === 'object' &&
typeof value.type === 'string' &&
typeof value.stream === 'function' &&
typeof value.arrayBuffer === 'function' &&
typeof value.constructor === 'function' &&
typeof value.constructor.name === 'string' &&
/^(Blob|File)$/.test(value.constructor.name) &&
/^(Blob|File)$/.test(value[Symbol.toStringTag])
);
};
export const isFormData = (value: any): value is FormData => {
return value instanceof FormData;
};
export const isSuccess = (status: number): boolean => {
return status >= 200 && status < 300;
};
export const base64 = (str: string): string => {
try {
return btoa(str);
} catch (err) {
// @ts-ignore
return Buffer.from(str).toString('base64');
}
};
export const getQueryString = (params: Record<string, any>): string => {
const qs: string[] = [];
const append = (key: string, value: any) => {
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
};
const process = (key: string, value: any) => {
if (isDefined(value)) {
if (Array.isArray(value)) {
value.forEach(v => {
process(key, v);
});
} else if (typeof value === 'object') {
Object.entries(value).forEach(([k, v]) => {
process(`${key}[${k}]`, v);
});
} else {
append(key, value);
}
}
};
Object.entries(params).forEach(([key, value]) => {
process(key, value);
});
if (qs.length > 0) {
return `?${qs.join('&')}`;
}
return '';
};
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
const encoder = config.ENCODE_PATH || encodeURI;
const path = options.url
.replace('{api-version}', config.VERSION)
.replace(/{(.*?)}/g, (substring: string, group: string) => {
if (options.path?.hasOwnProperty(group)) {
return encoder(String(options.path[group]));
}
return substring;
});
const url = `${config.BASE}${path}`;
if (options.query) {
return `${url}${getQueryString(options.query)}`;
}
return url;
};
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
if (options.formData) {
const formData = new FormData();
const process = (key: string, value: any) => {
if (isString(value) || isBlob(value)) {
formData.append(key, value);
} else {
formData.append(key, JSON.stringify(value));
}
};
Object.entries(options.formData)
.filter(([_, value]) => isDefined(value))
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => process(key, v));
} else {
process(key, value);
}
});
return formData;
}
return undefined;
};
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
export const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => {
if (typeof resolver === 'function') {
return (resolver as Resolver<T>)(options);
}
return resolver;
};
export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise<Record<string, string>> => {
const [token, username, password, additionalHeaders] = await Promise.all([
resolve(options, config.TOKEN),
resolve(options, config.USERNAME),
resolve(options, config.PASSWORD),
resolve(options, config.HEADERS),
]);
const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {}
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
...formHeaders,
})
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
}
if (options.body !== undefined) {
if (options.mediaType) {
headers['Content-Type'] = options.mediaType;
} else if (isBlob(options.body)) {
headers['Content-Type'] = options.body.type || 'application/octet-stream';
} else if (isString(options.body)) {
headers['Content-Type'] = 'text/plain';
} else if (!isFormData(options.body)) {
headers['Content-Type'] = 'application/json';
}
}
return headers;
};
export const getRequestBody = (options: ApiRequestOptions): any => {
if (options.body) {
return options.body;
}
return undefined;
};
export const sendRequest = async <T>(
config: OpenAPIConfig,
options: ApiRequestOptions,
url: string,
body: any,
formData: FormData | undefined,
headers: Record<string, string>,
onCancel: OnCancel,
axiosClient: AxiosInstance
): Promise<AxiosResponse<T>> => {
const source = axios.CancelToken.source();
const requestConfig: AxiosRequestConfig = {
url,
headers,
data: body ?? formData,
method: options.method,
withCredentials: config.WITH_CREDENTIALS,
withXSRFToken: config.CREDENTIALS === 'include' ? config.WITH_CREDENTIALS : false,
cancelToken: source.token,
};
onCancel(() => source.cancel('The user aborted a request.'));
try {
return await axiosClient.request(requestConfig);
} catch (error) {
const axiosError = error as AxiosError<T>;
if (axiosError.response) {
return axiosError.response;
}
throw error;
}
};
export const getResponseHeader = (response: AxiosResponse<any>, responseHeader?: string): string | undefined => {
if (responseHeader) {
const content = response.headers[responseHeader];
if (isString(content)) {
return content;
}
}
return undefined;
};
export const getResponseBody = (response: AxiosResponse<any>): any => {
if (response.status !== 204) {
return response.data;
}
return undefined;
};
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
...options.errors,
}
const error = errors[result.status];
if (error) {
throw new ApiError(options, result, error);
}
if (!result.ok) {
const errorStatus = result.status ?? 'unknown';
const errorStatusText = result.statusText ?? 'unknown';
const errorBody = (() => {
try {
return JSON.stringify(result.body, null, 2);
} catch (e) {
return undefined;
}
})();
throw new ApiError(options, result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
);
}
};
/**
* Request method
* @param config The OpenAPI configuration object
* @param options The request options from the service
* @param axiosClient The axios client instance to use
* @returns CancelablePromise<T>
* @throws ApiError
*/
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(config, options, formData);
if (!onCancel.isCancelled) {
const response = await sendRequest<T>(config, options, url, body, formData, headers, onCancel, axiosClient);
const responseBody = getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const result: ApiResult = {
url,
ok: isSuccess(response.status),
status: response.status,
statusText: response.statusText,
body: responseHeader ?? responseBody,
};
catchErrorCodes(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}
});
};

120
frontend/src/api/index.ts Normal file
View File

@@ -0,0 +1,120 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export { ApiError } from './core/ApiError';
export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI } from './core/OpenAPI';
export type { OpenAPIConfig } from './core/OpenAPI';
export type { ComboBoxItem } from './models/ComboBoxItem';
export type { ComboBoxOut } from './models/ComboBoxOut';
export type { CustomWebhook } from './models/CustomWebhook';
export type { DispatchIn } from './models/DispatchIn';
export type { GeneralConfig } from './models/GeneralConfig';
export type { GeneralConfig_Game } from './models/GeneralConfig_Game';
export type { GeneralConfig_Info } from './models/GeneralConfig_Info';
export type { GeneralConfig_Run } from './models/GeneralConfig_Run';
export type { GeneralConfig_Script } from './models/GeneralConfig_Script';
export type { GeneralUserConfig_Data } from './models/GeneralUserConfig_Data';
export type { GeneralUserConfig_Info } from './models/GeneralUserConfig_Info';
export type { GeneralUserConfig_Input } from './models/GeneralUserConfig_Input';
export type { GeneralUserConfig_Output } from './models/GeneralUserConfig_Output';
export { GetStageIn } from './models/GetStageIn';
export type { GlobalConfig_Function } from './models/GlobalConfig_Function';
export type { GlobalConfig_Input } from './models/GlobalConfig_Input';
export type { GlobalConfig_Notify } from './models/GlobalConfig_Notify';
export type { GlobalConfig_Output } from './models/GlobalConfig_Output';
export type { GlobalConfig_Start } from './models/GlobalConfig_Start';
export type { GlobalConfig_UI } from './models/GlobalConfig_UI';
export type { GlobalConfig_Update } from './models/GlobalConfig_Update';
export type { GlobalConfig_Voice } from './models/GlobalConfig_Voice';
export type { HistoryData } from './models/HistoryData';
export type { HistoryDataGetIn } from './models/HistoryDataGetIn';
export type { HistoryDataGetOut } from './models/HistoryDataGetOut';
export { HistoryIndexItem } from './models/HistoryIndexItem';
export { HistorySearchIn } from './models/HistorySearchIn';
export type { HistorySearchOut } from './models/HistorySearchOut';
export type { HTTPValidationError } from './models/HTTPValidationError';
export type { InfoOut } from './models/InfoOut';
export type { MaaConfig } from './models/MaaConfig';
export type { MaaConfig_Info } from './models/MaaConfig_Info';
export type { MaaConfig_Run } from './models/MaaConfig_Run';
export type { MaaPlanConfig } from './models/MaaPlanConfig';
export type { MaaPlanConfig_Info } from './models/MaaPlanConfig_Info';
export type { MaaPlanConfig_Item } from './models/MaaPlanConfig_Item';
export type { MaaUserConfig_Data } from './models/MaaUserConfig_Data';
export type { MaaUserConfig_Info } from './models/MaaUserConfig_Info';
export type { MaaUserConfig_Input } from './models/MaaUserConfig_Input';
export type { MaaUserConfig_Output } from './models/MaaUserConfig_Output';
export type { MaaUserConfig_Task } from './models/MaaUserConfig_Task';
export type { NoticeOut } from './models/NoticeOut';
export type { OutBase } from './models/OutBase';
export type { PlanCreateIn } from './models/PlanCreateIn';
export type { PlanCreateOut } from './models/PlanCreateOut';
export type { PlanDeleteIn } from './models/PlanDeleteIn';
export type { PlanGetIn } from './models/PlanGetIn';
export type { PlanGetOut } from './models/PlanGetOut';
export type { PlanIndexItem } from './models/PlanIndexItem';
export type { PlanReorderIn } from './models/PlanReorderIn';
export type { PlanUpdateIn } from './models/PlanUpdateIn';
export { PowerIn } from './models/PowerIn';
export type { QueueConfig } from './models/QueueConfig';
export type { QueueConfig_Info } from './models/QueueConfig_Info';
export type { QueueCreateOut } from './models/QueueCreateOut';
export type { QueueDeleteIn } from './models/QueueDeleteIn';
export type { QueueGetIn } from './models/QueueGetIn';
export type { QueueGetOut } from './models/QueueGetOut';
export type { QueueIndexItem } from './models/QueueIndexItem';
export type { QueueItem } from './models/QueueItem';
export type { QueueItem_Info } from './models/QueueItem_Info';
export type { QueueItemCreateOut } from './models/QueueItemCreateOut';
export type { QueueItemDeleteIn } from './models/QueueItemDeleteIn';
export type { QueueItemGetIn } from './models/QueueItemGetIn';
export type { QueueItemGetOut } from './models/QueueItemGetOut';
export type { QueueItemIndexItem } from './models/QueueItemIndexItem';
export type { QueueItemReorderIn } from './models/QueueItemReorderIn';
export type { QueueItemUpdateIn } from './models/QueueItemUpdateIn';
export type { QueueReorderIn } from './models/QueueReorderIn';
export type { QueueSetInBase } from './models/QueueSetInBase';
export type { QueueUpdateIn } from './models/QueueUpdateIn';
export { ScriptCreateIn } from './models/ScriptCreateIn';
export type { ScriptCreateOut } from './models/ScriptCreateOut';
export type { ScriptDeleteIn } from './models/ScriptDeleteIn';
export type { ScriptFileIn } from './models/ScriptFileIn';
export type { ScriptGetIn } from './models/ScriptGetIn';
export type { ScriptGetOut } from './models/ScriptGetOut';
export { ScriptIndexItem } from './models/ScriptIndexItem';
export type { ScriptReorderIn } from './models/ScriptReorderIn';
export type { ScriptUpdateIn } from './models/ScriptUpdateIn';
export type { ScriptUploadIn } from './models/ScriptUploadIn';
export type { ScriptUrlIn } from './models/ScriptUrlIn';
export type { SettingGetOut } from './models/SettingGetOut';
export type { SettingUpdateIn } from './models/SettingUpdateIn';
export { TaskCreateIn } from './models/TaskCreateIn';
export type { TaskCreateOut } from './models/TaskCreateOut';
export type { TimeSet } from './models/TimeSet';
export type { TimeSet_Info } from './models/TimeSet_Info';
export type { TimeSetCreateOut } from './models/TimeSetCreateOut';
export type { TimeSetDeleteIn } from './models/TimeSetDeleteIn';
export type { TimeSetGetIn } from './models/TimeSetGetIn';
export type { TimeSetGetOut } from './models/TimeSetGetOut';
export type { TimeSetIndexItem } from './models/TimeSetIndexItem';
export type { TimeSetReorderIn } from './models/TimeSetReorderIn';
export type { TimeSetUpdateIn } from './models/TimeSetUpdateIn';
export type { UpdateCheckIn } from './models/UpdateCheckIn';
export type { UpdateCheckOut } from './models/UpdateCheckOut';
export type { UserConfig_Notify } from './models/UserConfig_Notify';
export type { UserCreateOut } from './models/UserCreateOut';
export type { UserDeleteIn } from './models/UserDeleteIn';
export type { UserGetIn } from './models/UserGetIn';
export type { UserGetOut } from './models/UserGetOut';
export type { UserInBase } from './models/UserInBase';
export { UserIndexItem } from './models/UserIndexItem';
export type { UserReorderIn } from './models/UserReorderIn';
export type { UserSetIn } from './models/UserSetIn';
export type { UserUpdateIn } from './models/UserUpdateIn';
export type { ValidationError } from './models/ValidationError';
export type { VersionOut } from './models/VersionOut';
export { Service } from './services/Service';

100
frontend/src/api/mirrors.ts Normal file
View File

@@ -0,0 +1,100 @@
/**
* 镜像源 API 接口
* 用于从后端获取最新的镜像源配置
*/
import { OpenAPI } from '@/api'
import type { MirrorConfig } from '@/config/mirrors'
export interface MirrorApiResponse {
git?: MirrorConfig[]
python?: MirrorConfig[]
pip?: MirrorConfig[]
apiEndpoints?: {
local?: string
production?: string
proxy?: string
}
downloadLinks?: {
[category: string]: {
[key: string]: string
}
}
}
/**
* 获取镜像源配置
*/
export async function fetchMirrorConfig(): Promise<MirrorApiResponse> {
try {
const response = await fetch(`${OpenAPI.BASE}/api/mirrors`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
} catch (error) {
console.warn('获取镜像源配置失败,使用默认配置:', error)
throw error
}
}
/**
* 测试镜像源连通性
*/
export async function testMirrorConnectivity(url: string, timeout: number = 5000): Promise<{
success: boolean
speed: number
error?: string
}> {
try {
const startTime = Date.now()
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
const response = await fetch(url, {
method: 'HEAD',
signal: controller.signal,
cache: 'no-cache',
mode: 'no-cors' // 避免 CORS 问题
})
clearTimeout(timeoutId)
const speed = Date.now() - startTime
return {
success: true,
speed
}
} catch (error) {
return {
success: false,
speed: 9999,
error: error instanceof Error ? error.message : String(error)
}
}
}
/**
* 批量测试镜像源
*/
export async function batchTestMirrors(mirrors: MirrorConfig[]): Promise<MirrorConfig[]> {
const promises = mirrors.map(async (mirror) => {
const result = await testMirrorConnectivity(mirror.url)
return {
...mirror,
speed: result.speed
}
})
const results = await Promise.all(promises)
// 按速度排序
return results.sort((a, b) => (a.speed || 9999) - (b.speed || 9999))
}

View File

@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ComboBoxItem = {
/**
* 展示值
*/
label: string;
/**
* 实际值
*/
value: (string | null);
};

View File

@@ -0,0 +1,24 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ComboBoxItem } from './ComboBoxItem';
export type ComboBoxOut = {
/**
* 状态码
*/
code?: number;
/**
* 操作状态
*/
status?: string;
/**
* 操作消息
*/
message?: string;
/**
* 下拉框选项
*/
data: Array<ComboBoxItem>;
};

View File

@@ -0,0 +1,35 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CustomWebhook = {
/**
* Webhook唯一标识
*/
id: string;
/**
* Webhook名称
*/
name: string;
/**
* Webhook URL
*/
url: string;
/**
* 消息模板
*/
template: string;
/**
* 是否启用
*/
enabled?: boolean;
/**
* 自定义请求头
*/
headers?: (Record<string, string> | null);
/**
* 请求方法
*/
method?: ('POST' | 'GET' | null);
};

View File

@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DispatchIn = {
/**
* 目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID
*/
taskId: string;
};

View File

@@ -0,0 +1,27 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GeneralConfig_Game } from './GeneralConfig_Game';
import type { GeneralConfig_Info } from './GeneralConfig_Info';
import type { GeneralConfig_Run } from './GeneralConfig_Run';
import type { GeneralConfig_Script } from './GeneralConfig_Script';
export type GeneralConfig = {
/**
* 脚本基础信息
*/
Info?: (GeneralConfig_Info | null);
/**
* 脚本配置
*/
Script?: (GeneralConfig_Script | null);
/**
* 游戏配置
*/
Game?: (GeneralConfig_Game | null);
/**
* 运行配置
*/
Run?: (GeneralConfig_Run | null);
};

View File

@@ -0,0 +1,31 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GeneralConfig_Game = {
/**
* 游戏/模拟器相关功能是否启用
*/
Enabled?: (boolean | null);
/**
* 类型: 模拟器, PC端
*/
Type?: ('Emulator' | 'Client' | null);
/**
* 游戏/模拟器程序路径
*/
Path?: (string | null);
/**
* 游戏/模拟器启动参数
*/
Arguments?: (string | null);
/**
* 游戏/模拟器等待启动时间
*/
WaitTime?: (number | null);
/**
* 是否强制关闭游戏/模拟器进程
*/
IfForceClose?: (boolean | null);
};

View File

@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GeneralConfig_Info = {
/**
* 脚本名称
*/
Name?: (string | null);
/**
* 脚本根目录
*/
RootPath?: (string | null);
};

View File

@@ -0,0 +1,19 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GeneralConfig_Run = {
/**
* 每日代理次数限制
*/
ProxyTimesLimit?: (number | null);
/**
* 重试次数限制
*/
RunTimesLimit?: (number | null);
/**
* 日志超时限制
*/
RunTimeLimit?: (number | null);
};

View File

@@ -0,0 +1,58 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GeneralConfig_Script = {
/**
* 脚本可执行文件路径
*/
ScriptPath?: string | null
/**
* 脚本启动附加命令参数
*/
Arguments?: string | null
/**
* 是否追踪脚本子进程
*/
IfTrackProcess?: boolean | null
/**
* 配置文件路径
*/
ConfigPath?: string | null
/**
* 配置文件类型: 单个文件, 文件夹
*/
ConfigPathMode?: 'File' | 'Folder' | null
/**
* 更新配置时机, 从不, 仅成功时, 仅失败时, 任务结束时
*/
UpdateConfigMode?: 'Never' | 'Success' | 'Failure' | 'Always' | null
/**
* 日志文件路径
*/
LogPath?: string | null
/**
* 日志文件名格式
*/
LogPathFormat?: string | null
/**
* 日志时间戳开始位置
*/
LogTimeStart?: number | null
/**
* 日志时间戳结束位置
*/
LogTimeEnd?: number | null
/**
* 日志时间戳格式
*/
LogTimeFormat?: string | null
/**
* 成功时日志
*/
SuccessLog?: string | null
/**
* 错误时日志
*/
ErrorLog?: string | null
}

View File

@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GeneralUserConfig_Data = {
/**
* 上次代理日期
*/
LastProxyDate?: (string | null);
/**
* 代理次数
*/
ProxyTimes?: (number | null);
};

View File

@@ -0,0 +1,39 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GeneralUserConfig_Info = {
/**
* 用户名
*/
Name?: (string | null);
/**
* 用户状态
*/
Status?: (boolean | null);
/**
* 剩余天数
*/
RemainedDay?: (number | null);
/**
* 是否在任务前执行脚本
*/
IfScriptBeforeTask?: (boolean | null);
/**
* 任务前脚本路径
*/
ScriptBeforeTask?: (string | null);
/**
* 是否在任务后执行脚本
*/
IfScriptAfterTask?: (boolean | null);
/**
* 任务后脚本路径
*/
ScriptAfterTask?: (string | null);
/**
* 备注
*/
Notes?: (string | null);
};

View File

@@ -0,0 +1,22 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GeneralUserConfig_Data } from './GeneralUserConfig_Data';
import type { GeneralUserConfig_Info } from './GeneralUserConfig_Info';
import type { UserConfig_Notify } from './UserConfig_Notify';
export type GeneralUserConfig_Input = {
/**
* 用户信息
*/
Info?: (GeneralUserConfig_Info | null);
/**
* 用户数据
*/
Data?: (GeneralUserConfig_Data | null);
/**
* 单独通知
*/
Notify?: (UserConfig_Notify | null);
};

View File

@@ -0,0 +1,22 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GeneralUserConfig_Data } from './GeneralUserConfig_Data';
import type { GeneralUserConfig_Info } from './GeneralUserConfig_Info';
import type { UserConfig_Notify } from './UserConfig_Notify';
export type GeneralUserConfig_Output = {
/**
* 用户信息
*/
Info?: (GeneralUserConfig_Info | null);
/**
* 用户数据
*/
Data?: (GeneralUserConfig_Data | null);
/**
* 单独通知
*/
Notify?: (UserConfig_Notify | null);
};

View File

@@ -0,0 +1,27 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GetStageIn = {
/**
* 选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项
*/
type: GetStageIn.type;
};
export namespace GetStageIn {
/**
* 选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项
*/
export enum type {
TODAY = 'Today',
ALL = 'ALL',
MONDAY = 'Monday',
TUESDAY = 'Tuesday',
WEDNESDAY = 'Wednesday',
THURSDAY = 'Thursday',
FRIDAY = 'Friday',
SATURDAY = 'Saturday',
SUNDAY = 'Sunday',
}
}

View File

@@ -0,0 +1,31 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GlobalConfig_Function = {
/**
* 历史记录保留时间, 0表示永久保存
*/
HistoryRetentionTime?: (7 | 15 | 30 | 60 | 90 | 180 | 365 | 0 | null);
/**
* 允许休眠
*/
IfAllowSleep?: (boolean | null);
/**
* 静默模式
*/
IfSilence?: (boolean | null);
/**
* 模拟器老板键
*/
BossKey?: (string | null);
/**
* 同意哔哩哔哩用户协议
*/
IfAgreeBilibili?: (boolean | null);
/**
* 跳过Mumu模拟器启动广告
*/
IfSkipMumuSplashAds?: (boolean | null);
};

View File

@@ -0,0 +1,37 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GlobalConfig_Function } from './GlobalConfig_Function';
import type { GlobalConfig_Notify } from './GlobalConfig_Notify';
import type { GlobalConfig_Start } from './GlobalConfig_Start';
import type { GlobalConfig_UI } from './GlobalConfig_UI';
import type { GlobalConfig_Update } from './GlobalConfig_Update';
import type { GlobalConfig_Voice } from './GlobalConfig_Voice';
export type GlobalConfig_Input = {
/**
* 功能相关配置
*/
Function?: (GlobalConfig_Function | null);
/**
* 语音相关配置
*/
Voice?: (GlobalConfig_Voice | null);
/**
* 启动相关配置
*/
Start?: (GlobalConfig_Start | null);
/**
* 界面相关配置
*/
UI?: (GlobalConfig_UI | null);
/**
* 通知相关配置
*/
Notify?: (GlobalConfig_Notify | null);
/**
* 更新相关配置
*/
Update?: (GlobalConfig_Update | null);
};

View File

@@ -0,0 +1,56 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { CustomWebhook } from './CustomWebhook';
export type GlobalConfig_Notify = {
/**
* 任务结果推送时机
*/
SendTaskResultTime?: ('不推送' | '任何时刻' | '仅失败时' | null);
/**
* 是否发送统计信息
*/
IfSendStatistic?: (boolean | null);
/**
* 是否发送公招六星通知
*/
IfSendSixStar?: (boolean | null);
/**
* 是否推送系统通知
*/
IfPushPlyer?: (boolean | null);
/**
* 是否发送邮件通知
*/
IfSendMail?: (boolean | null);
/**
* SMTP服务器地址
*/
SMTPServerAddress?: (string | null);
/**
* SMTP授权码
*/
AuthorizationCode?: (string | null);
/**
* 邮件发送地址
*/
FromAddress?: (string | null);
/**
* 邮件接收地址
*/
ToAddress?: (string | null);
/**
* 是否使用ServerChan推送
*/
IfServerChan?: (boolean | null);
/**
* ServerChan推送密钥
*/
ServerChanKey?: (string | null);
/**
* 自定义Webhook列表
*/
CustomWebhooks?: (Array<CustomWebhook> | null);
};

View File

@@ -0,0 +1,37 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GlobalConfig_Function } from './GlobalConfig_Function';
import type { GlobalConfig_Notify } from './GlobalConfig_Notify';
import type { GlobalConfig_Start } from './GlobalConfig_Start';
import type { GlobalConfig_UI } from './GlobalConfig_UI';
import type { GlobalConfig_Update } from './GlobalConfig_Update';
import type { GlobalConfig_Voice } from './GlobalConfig_Voice';
export type GlobalConfig_Output = {
/**
* 功能相关配置
*/
Function?: (GlobalConfig_Function | null);
/**
* 语音相关配置
*/
Voice?: (GlobalConfig_Voice | null);
/**
* 启动相关配置
*/
Start?: (GlobalConfig_Start | null);
/**
* 界面相关配置
*/
UI?: (GlobalConfig_UI | null);
/**
* 通知相关配置
*/
Notify?: (GlobalConfig_Notify | null);
/**
* 更新相关配置
*/
Update?: (GlobalConfig_Update | null);
};

View File

@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type GlobalConfig_Start = {
/**
* 是否在系统启动时自动运行
*/
IfSelfStart?: (boolean | null);
/**
* 启动时是否直接最小化到托盘而不显示主窗口
*/
IfMinimizeDirectly?: (boolean | null);
};

Some files were not shown because too many files have changed in this diff Show More