diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml index a372406..575e65c 100644 --- a/.github/workflows/build-app.yml +++ b/.github/workflows/build-app.yml @@ -111,7 +111,7 @@ jobs: NOTES_MAIN="$(sed 's/\r$//g' <(tail -n +3 version_info.txt))" NOTES="$NOTES_MAIN - [已有 Mirror酱 CDK ?前往 Mirror酱 高速下载](https://mirrorchyan.com/zh/projects?rid=AUTO_MAA) + [已有 Mirror酱 CDK ?前往 Mirror酱 高速下载](https://mirrorchyan.com/zh/projects?rid=AUTO_MAA&source=auto_maa-release) \`\`\`本release通过GitHub Actions自动构建\`\`\`" if [ "${{ github.ref_name }}" == "main" ]; then diff --git a/README.md b/README.md index aae586b..aa5331d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ GitHub Contributors GitHub License DeepWiki - mirrorc + mirrorc

## 软件介绍 diff --git a/app/core/config.py b/app/core/config.py index 2e1311a..00ea92d 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -426,11 +426,14 @@ class MaaUserConfig(LQConfig): self.Info_GameId_1 = ConfigItem("Info", "GameId_1", "-") self.Info_GameId_2 = ConfigItem("Info", "GameId_2", "-") self.Info_GameId_Remain = ConfigItem("Info", "GameId_Remain", "-") + self.Info_IfSkland = ConfigItem("Info", "IfSkland", False, BoolValidator()) + self.Info_SklandToken = ConfigItem("Info", "SklandToken", "") self.Data_LastProxyDate = ConfigItem("Data", "LastProxyDate", "2000-01-01") self.Data_LastAnnihilationDate = ConfigItem( "Data", "LastAnnihilationDate", "2000-01-01" ) + self.Data_LastSklandDate = ConfigItem("Data", "LastSklandDate", "2000-01-01") self.Data_ProxyTimes = ConfigItem( "Data", "ProxyTimes", 0, RangeValidator(0, 1024) ) @@ -572,7 +575,7 @@ class MaaPlanConfig(LQConfig): class AppConfig(GlobalConfig): - VERSION = "4.3.9.0" + VERSION = "4.3.10.2" gameid_refreshed = Signal() PASSWORD_refreshed = Signal() @@ -1256,6 +1259,10 @@ class AppConfig(GlobalConfig): user_config.Data_LastAnnihilationDate, info["Config"]["Data"]["LastAnnihilationDate"], ) + user_config.set( + user_config.Data_LastSklandDate, + info["Config"]["Data"]["LastSklandDate"], + ) user_config.set( user_config.Data_ProxyTimes, info["Config"]["Data"]["ProxyTimes"] ) diff --git a/app/models/MAA.py b/app/models/MAA.py index 0ea319e..f41bd56 100644 --- a/app/models/MAA.py +++ b/app/models/MAA.py @@ -30,7 +30,6 @@ from PySide6.QtCore import QObject, Signal, QEventLoop, QFileSystemWatcher, QTim import json import subprocess import shutil -import time import re import win32com.client from datetime import datetime, timedelta @@ -39,7 +38,7 @@ from jinja2 import Environment, FileSystemLoader from typing import Union, List, Dict from app.core import Config, MaaConfig, MaaUserConfig -from app.services import Notify, System +from app.services import Notify, Crypto, System, skland_sign_in class MaaManager(QObject): @@ -220,6 +219,51 @@ class MaaManager(QObject): user_logs_list = [] user_start_time = datetime.now() + if user_data["Info"]["IfSkland"] and user_data["Info"]["SklandToken"]: + + if user_data["Data"]["LastSklandDate"] != datetime.now().strftime( + "%Y-%m-%d" + ): + + self.update_log_text.emit("正在执行森空岛签到中\n请稍候~") + + skland_result = skland_sign_in( + Crypto.win_decryptor(user_data["Info"]["SklandToken"]) + ) + + for type, user_list in skland_result.items(): + + if type != "总计" and len(user_list) > 0: + + logger.info( + f"{self.name} | 用户: {user[0]} - 森空岛签到{type}: {'、'.join(user_list)}" + ) + self.push_info_bar.emit( + "info", + f"森空岛签到{type}", + "、".join(user_list), + -1, + ) + + if ( + skland_result["总计"] > 0 + and len(skland_result["失败"]) == 0 + ): + user_data["Data"][ + "LastSklandDate" + ] = datetime.now().strftime("%Y-%m-%d") + self.play_sound.emit("森空岛签到成功") + else: + self.play_sound.emit("森空岛签到失败") + + elif user_data["Info"]["IfSkland"]: + logger.warning( + f"{self.name} | 用户: {user[0]} - 未配置森空岛签到Token,跳过森空岛签到" + ) + self.push_info_bar.emit( + "warning", "森空岛签到失败", "未配置鹰角网络通行证登录凭证", -1 + ) + # 剿灭-日常模式循环 for mode in ["Annihilation", "Routine"]: @@ -910,6 +954,9 @@ class MaaManager(QObject): self.sleep(self.wait_time) + if self.isInterruptionRequested: + return None + # 移除静默进程标记 Config.silence_list.remove(self.emulator_path) @@ -979,7 +1026,8 @@ class MaaManager(QObject): else: logger.info(f"{self.name} | 无法连接到ADB地址:{ADB_address}") - self.play_sound.emit("ADB失败") + if not self.isInterruptionRequested: + self.play_sound.emit("ADB失败") def refresh_maa_log(self) -> None: """刷新MAA日志""" diff --git a/app/services/__init__.py b/app/services/__init__.py index 8333f50..9c018a1 100644 --- a/app/services/__init__.py +++ b/app/services/__init__.py @@ -32,5 +32,6 @@ __license__ = "GPL-3.0 license" from .notification import Notify from .security import Crypto from .system import System +from .skland import skland_sign_in -__all__ = ["Notify", "Crypto", "System"] +__all__ = ["Notify", "Crypto", "System", "skland_sign_in"] diff --git a/app/services/skland.py b/app/services/skland.py new file mode 100644 index 0000000..ff59014 --- /dev/null +++ b/app/services/skland.py @@ -0,0 +1,239 @@ +# AUTO_MAA:A MAA Multi Account Management and Automation Tool +# Copyright © 2024-2025 DLmaster361 + +# This file incorporates work covered by the following copyright and +# permission notice: +# +# skland-checkin-ghaction Copyright © 2023 Yanstory +# https://github.com/Yanstory/skland-checkin-ghaction + +# This file is part of AUTO_MAA. + +# AUTO_MAA is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, +# or (at your option) any later version. + +# AUTO_MAA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty +# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +# the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with AUTO_MAA. If not, see . + +# Contact: DLmaster_361@163.com + + +""" +AUTO_MAA +AUTO_MAA森空岛服务 +v4.3 +作者:DLmaster_361、ClozyA +""" + +from loguru import logger +import time +import json +import hmac +import hashlib +import requests +from urllib import parse + + +def skland_sign_in(token) -> dict: + """森空岛签到""" + + app_code = "4ca99fa6b56cc2ba" + # 用于获取grant code + grant_code_url = "https://as.hypergryph.com/user/oauth2/v2/grant" + # 用于获取cred + cred_code_url = "https://zonai.skland.com/api/v1/user/auth/generate_cred_by_code" + # 查询角色绑定 + binding_url = "https://zonai.skland.com/api/v1/game/player/binding" + # 签到接口 + sign_url = "https://zonai.skland.com/api/v1/game/attendance" + + # 基础请求头 + header = { + "cred": "", + "User-Agent": "Skland/1.5.1 (com.hypergryph.skland; build:100501001; Android 34;) Okhttp/4.11.0", + "Accept-Encoding": "gzip", + "Connection": "close", + } + header_login = header.copy() + header_for_sign = { + "platform": "1", + "timestamp": "", + "dId": "", + "vName": "1.5.1", + } + + # 生成签名 + def generate_signature(token_for_sign: str, path, body_or_query): + """ + 生成请求签名 + :param token_for_sign: 用于加密的token + :param path: 请求路径(如 /api/v1/game/player/binding) + :param body_or_query: GET用query字符串,POST用body字符串 + :return: (sign, 新的header_for_sign字典) + """ + t = str(int(time.time()) - 2) # 时间戳,-2秒以防服务器时间不一致 + token_bytes = token_for_sign.encode("utf-8") + header_ca = dict(header_for_sign) + header_ca["timestamp"] = t + header_ca_str = json.dumps(header_ca, separators=(",", ":")) + s = path + body_or_query + t + header_ca_str # 拼接原始字符串 + # HMAC-SHA256 + MD5得到最终sign + hex_s = hmac.new(token_bytes, s.encode("utf-8"), hashlib.sha256).hexdigest() + md5 = hashlib.md5(hex_s.encode("utf-8")).hexdigest() + return md5, header_ca + + # 获取带签名的header + def get_sign_header(url: str, method, body, old_header, sign_token): + """ + 获取带签名的请求头 + :param url: 请求完整url + :param method: 请求方式 GET/POST + :param body: POST请求体或GET时为None + :param old_header: 原始请求头 + :param sign_token: 当前会话的签名token + :return: 新请求头 + """ + h = json.loads(json.dumps(old_header)) + p = parse.urlparse(url) + if method.lower() == "get": + sign, header_ca = generate_signature(sign_token, p.path, p.query) + else: + sign, header_ca = generate_signature( + sign_token, p.path, json.dumps(body) if body else "" + ) + h["sign"] = sign + for i in header_ca: + h[i] = header_ca[i] + return h + + # 复制请求头并添加cred + def copy_header(cred): + v = json.loads(json.dumps(header)) + v["cred"] = cred + return v + + # 使用token一步步拿到cred和sign_token + def login_by_token(token_code): + """ + :param token_code: 你的skyland token + :return: (cred, sign_token) + """ + try: + # token为json对象时提取data.content + t = json.loads(token_code) + token_code = t["data"]["content"] + except: + pass + grant_code = get_grant_code(token_code) + return get_cred(grant_code) + + # 通过grant code换cred和sign_token + def get_cred(grant): + rsp = requests.post( + cred_code_url, json={"code": grant, "kind": 1}, headers=header_login + ).json() + if rsp["code"] != 0: + raise Exception(f'获得cred失败:{rsp.get("messgae")}') + sign_token = rsp["data"]["token"] + cred = rsp["data"]["cred"] + return cred, sign_token + + # 通过token换grant code + def get_grant_code(token): + rsp = requests.post( + grant_code_url, + json={"appCode": app_code, "token": token, "type": 0}, + headers=header_login, + ).json() + if rsp["status"] != 0: + raise Exception(f'使用token: {token} 获得认证代码失败:{rsp.get("msg")}') + return rsp["data"]["code"] + + # 获取已绑定的角色列表 + def get_binding_list(cred, sign_token): + """ + 查询绑定的角色 + :param cred: 当前cred + :param sign_token: 当前sign_token + :return: 角色列表 + """ + v = [] + rsp = requests.get( + binding_url, + headers=get_sign_header( + binding_url, "get", None, copy_header(cred), sign_token + ), + ).json() + if rsp["code"] != 0: + logger.error(f"森空岛服务 | 请求角色列表出现问题:{rsp['message']}") + if rsp.get("message") == "用户未登录": + logger.error(f"森空岛服务 | 用户登录可能失效了,请重新登录!") + return v + # 只取明日方舟(arknights)的绑定账号 + for i in rsp["data"]["list"]: + if i.get("appCode") != "arknights": + continue + v.extend(i.get("bindingList")) + return v + + # 执行签到 + def do_sign(cred, sign_token) -> dict: + """ + 对所有绑定的角色进行签到 + :param cred: 当前cred + :param sign_token: 当前sign_token + :return: 签到结果字典 + """ + + characters = get_binding_list(cred, sign_token) + result = {"成功": [], "重复": [], "失败": [], "总计": len(characters)} + + for character in characters: + + body = { + "uid": character.get("uid"), + "gameId": character.get("channelMasterId"), + } + rsp = requests.post( + sign_url, + headers=get_sign_header( + sign_url, "post", body, copy_header(cred), sign_token + ), + json=body, + ).json() + + if rsp["code"] != 0: + + result[ + "重复" if rsp.get("message") == "请勿重复签到!" else "失败" + ].append( + f"{character.get("nickName")}({character.get("channelName")})" + ) + + else: + + result["成功"].append( + f"{character.get("nickName")}({character.get("channelName")})" + ) + + time.sleep(3) + + return result + + # 主流程 + try: + # 拿到cred和sign_token + cred, sign_token = login_by_token(token) + time.sleep(1) + # 依次签到 + return do_sign(cred, sign_token) + except Exception as e: + logger.error(f"森空岛服务 | 森空岛签到失败: {e}") + return {"成功": [], "重复": [], "失败": [], "总计": 0} diff --git a/app/ui/Widget.py b/app/ui/Widget.py index 16b40be..c810ccf 100644 --- a/app/ui/Widget.py +++ b/app/ui/Widget.py @@ -1,6 +1,12 @@ # AUTO_MAA:A MAA Multi Account Management and Automation Tool # Copyright © 2024-2025 DLmaster361 +# This file incorporates work covered by the following copyright and +# permission notice: +# +# ZenlessZoneZero-OneDragon Copyright © 2024-2025 DoctorReid +# https://github.com/DoctorReid/ZenlessZoneZero-OneDragon + # This file is part of AUTO_MAA. # AUTO_MAA is free software: you can redistribute it and/or modify @@ -608,6 +614,83 @@ class PushAndSwitchButtonSettingCard(SettingCard): self.switchButton.setText("开" if isChecked else "关") +class PasswordLineAndSwitchButtonSettingCard(SettingCard): + """Setting card with PasswordLineEdit and SwitchButton""" + + textChanged = Signal() + + def __init__( + self, + icon: Union[str, QIcon, FluentIconBase], + title: str, + content: Union[str, None], + text: str, + algorithm: str, + qconfig: QConfig, + configItem_bool: ConfigItem, + configItem_info: ConfigItem, + parent=None, + ): + + super().__init__(icon, title, content, parent) + self.algorithm = algorithm + self.qconfig = qconfig + self.configItem_bool = configItem_bool + self.configItem_info = configItem_info + self.LineEdit = PasswordLineEdit(self) + self.LineEdit.setMinimumWidth(200) + self.LineEdit.setPlaceholderText(text) + if algorithm == "AUTO": + self.LineEdit.setViewPasswordButtonVisible(False) + self.SwitchButton = SwitchButton(self) + + self.hBoxLayout.addWidget(self.LineEdit, 0, Qt.AlignRight) + self.hBoxLayout.addSpacing(16) + self.hBoxLayout.addWidget(self.SwitchButton, 0, Qt.AlignRight) + self.hBoxLayout.addSpacing(16) + + self.configItem_info.valueChanged.connect(self.setInfo) + self.LineEdit.textChanged.connect(self.__textChanged) + self.configItem_bool.valueChanged.connect(self.SwitchButton.setChecked) + self.SwitchButton.checkedChanged.connect( + lambda isChecked: self.qconfig.set(self.configItem_bool, isChecked) + ) + + self.setInfo(self.qconfig.get(configItem_info)) + self.SwitchButton.setChecked(self.qconfig.get(configItem_bool)) + + def __textChanged(self, content: str): + + self.configItem_info.valueChanged.disconnect(self.setInfo) + if self.algorithm == "DPAPI": + self.qconfig.set(self.configItem_info, Crypto.win_encryptor(content)) + elif self.algorithm == "AUTO": + self.qconfig.set(self.configItem_info, Crypto.AUTO_encryptor(content)) + self.configItem_info.valueChanged.connect(self.setInfo) + + self.textChanged.emit() + + def setInfo(self, content: str): + + self.LineEdit.textChanged.disconnect(self.__textChanged) + if self.algorithm == "DPAPI": + self.LineEdit.setText(Crypto.win_decryptor(content)) + elif self.algorithm == "AUTO": + if Crypto.check_PASSWORD(Config.PASSWORD): + self.LineEdit.setText(Crypto.AUTO_decryptor(content, Config.PASSWORD)) + self.LineEdit.setPasswordVisible(True) + self.LineEdit.setReadOnly(False) + elif Config.PASSWORD: + self.LineEdit.setText("管理密钥错误") + self.LineEdit.setPasswordVisible(True) + self.LineEdit.setReadOnly(True) + else: + self.LineEdit.setText("************") + self.LineEdit.setPasswordVisible(False) + self.LineEdit.setReadOnly(True) + self.LineEdit.textChanged.connect(self.__textChanged) + + class PushAndComboBoxSettingCard(SettingCard): """Setting card with push & combo box""" @@ -1129,6 +1212,13 @@ class UserLableSettingCard(SettingCard): == Config.server_date().isocalendar()[:2] else "本周剿灭未完成" ) + if self.qconfig.get(self.configItems["IfSkland"]): + text_list.append( + "森空岛已签到" + if datetime.now().strftime("%Y-%m-%d") + == self.qconfig.get(self.configItems["LastSklandDate"]) + else "森空岛未签到" + ) self.Lable.setText(" | ".join(text_list)) diff --git a/app/ui/home.py b/app/ui/home.py index 475230a..00624a6 100644 --- a/app/ui/home.py +++ b/app/ui/home.py @@ -405,4 +405,6 @@ class ButtonGroup(SimpleCardWidget): def open_sales(self): """打开 MirrorChyan 链接""" - QDesktopServices.openUrl(QUrl("https://mirrorchyan.com/")) + QDesktopServices.openUrl( + QUrl("https://mirrorchyan.com/zh/get-start?source=auto_maa-home") + ) diff --git a/app/ui/member_manager.py b/app/ui/member_manager.py index fec31d6..8942848 100644 --- a/app/ui/member_manager.py +++ b/app/ui/member_manager.py @@ -80,6 +80,7 @@ from .Widget import ( EditableComboBoxWithPlanSettingCard, SpinBoxWithPlanSettingCard, PasswordLineEditSettingCard, + PasswordLineAndSwitchButtonSettingCard, UserLableSettingCard, UserTaskSettingCard, ComboBoxSettingCard, @@ -1585,6 +1586,18 @@ class MemberManager(QWidget): parent=self, ) ) + self.card_Skland = PasswordLineAndSwitchButtonSettingCard( + icon=FluentIcon.CERTIFICATE, + title="森空岛签到", + content="此功能具有一定风险,请谨慎使用!获取登录凭证请查阅「文档-进阶功能」。", + text="鹰角网络通行证登录凭证", + algorithm="DPAPI", + qconfig=self.config, + configItem_bool=self.config.Info_IfSkland, + configItem_info=self.config.Info_SklandToken, + parent=self, + ) + self.card_Skland.LineEdit.setMinimumWidth(250) self.card_UserLable = UserLableSettingCard( icon=FluentIcon.INFO, @@ -1596,6 +1609,8 @@ class MemberManager(QWidget): "LastAnnihilationDate": self.config.Data_LastAnnihilationDate, "ProxyTimes": self.config.Data_ProxyTimes, "IfPassCheck": self.config.Data_IfPassCheck, + "IfSkland": self.config.Info_IfSkland, + "LastSklandDate": self.config.Data_LastSklandDate, }, parent=self, ) @@ -1778,6 +1793,7 @@ class MemberManager(QWidget): Layout.addLayout(h6_layout) Layout.addLayout(h7_layout) Layout.addLayout(h8_layout) + Layout.addWidget(self.card_Skland) Layout.addWidget(self.card_TaskSet) Layout.addWidget(self.card_NotifySet) diff --git a/app/ui/setting.py b/app/ui/setting.py index d9542ee..9de2858 100644 --- a/app/ui/setting.py +++ b/app/ui/setting.py @@ -1093,7 +1093,9 @@ class UpdaterSettingCard(HeaderCardWidget): parent=self, ) mirrorchyan_url = HyperlinkButton( - "https://mirrorchyan.com/", "获取Mirror酱CDK", self + "https://mirrorchyan.com/zh/get-start?source=auto_maa-setting_card", + "获取Mirror酱CDK", + self, ) self.card_MirrorChyanCDK.hBoxLayout.insertWidget( 5, mirrorchyan_url, 0, Qt.AlignRight diff --git a/resources/html/MAA_six_star.html b/resources/html/MAA_six_star.html index 38b461a..ef38222 100644 --- a/resources/html/MAA_six_star.html +++ b/resources/html/MAA_six_star.html @@ -1,129 +1,26 @@ - + - + img { + display: block; + max-width: 100%; + height: auto; + margin: 0 auto; + } + -
-
-

喜报!

- - -
- -
-

恭喜用户 {{ user_name }} 喜提公开招募六星高资!

-
- -

AUTO_MAA 敬上

- - - -
+ Base64 Image \ No newline at end of file diff --git a/resources/sounds/noisy/森空岛签到失败.wav b/resources/sounds/noisy/森空岛签到失败.wav new file mode 100644 index 0000000..4ad31d5 Binary files /dev/null and b/resources/sounds/noisy/森空岛签到失败.wav differ diff --git a/resources/sounds/noisy/森空岛签到成功.wav b/resources/sounds/noisy/森空岛签到成功.wav new file mode 100644 index 0000000..fd6319f Binary files /dev/null and b/resources/sounds/noisy/森空岛签到成功.wav differ diff --git a/resources/version.json b/resources/version.json index daee41c..589d206 100644 --- a/resources/version.json +++ b/resources/version.json @@ -1,26 +1,17 @@ { - "main_version": "4.3.9.0", + "main_version": "4.3.10.2", "version_info": { - "4.3.9.0": { - "修复bug": [ - "修复网络模块子线程未及时销毁导致的程序崩溃" - ] - }, - "4.3.9.2": { - "修复bug": [ - "修复语音包禁忌二重奏" - ] - }, - "4.3.9.1": { + "4.3.10.2": { "新增功能": [ - "语音功能上线" - ], - "修复bug": [ - "网络模块支持并发请求", - "修复中止任务时程序异常卡顿" + "公招喜报模板优化" ], "程序优化": [ - "非UI组件转为QObject类" + "Mirror 酱链接添加`source`字段,用于标识来源" + ] + }, + "4.3.10.1": { + "新增功能": [ + "森空岛签到功能上线" ] } }