Merge branch 'dev'

This commit is contained in:
DLmaster361
2025-06-05 19:10:49 +08:00
14 changed files with 439 additions and 146 deletions

View File

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

View File

@@ -14,7 +14,7 @@
<a href="https://github.com/DLmaster361/AUTO_MAA/graphs/contributors"><img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/DLmaster361/AUTO_MAA?style=flat-square"></a>
<a href="https://github.com/DLmaster361/AUTO_MAA/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/DLmaster361/AUTO_MAA?style=flat-square"></a>
<a href="https://deepwiki.com/DLmaster361/AUTO_MAA"><img alt="DeepWiki" src="https://deepwiki.com/badge.svg"></a>
<a href="https://mirrorchyan.com/zh/projects?rid=AUTO_MAA"><img alt="mirrorc" src="https://img.shields.io/badge/Mirror%E9%85%B1-%239af3f6?logo=countingworkspro&logoColor=4f46e5"></a>
<a href="https://mirrorchyan.com/zh/projects?rid=AUTO_MAA&source=auto_maa-readme"><img alt="mirrorc" src="https://img.shields.io/badge/Mirror%E9%85%B1-%239af3f6?logo=countingworkspro&logoColor=4f46e5"></a>
</p>
## 软件介绍

View File

@@ -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"]
)

View File

@@ -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日志"""

View File

@@ -32,5 +32,6 @@ __license__ = "GPL-3.0 license"
from .notification import Notify
from .security import Crypto
from .system import System
from .skland import skland_sign_in
__all__ = ["Notify", "Crypto", "System"]
__all__ = ["Notify", "Crypto", "System", "skland_sign_in"]

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

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

View File

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

View File

@@ -405,4 +405,6 @@ class ButtonGroup(SimpleCardWidget):
def open_sales(self):
"""打开 MirrorChyan 链接"""
QDesktopServices.openUrl(QUrl("https://mirrorchyan.com/"))
QDesktopServices.openUrl(
QUrl("https://mirrorchyan.com/zh/get-start?source=auto_maa-home")
)

View File

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

View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@@ -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": {
"新增功能": [
"森空岛签到功能上线"
]
}
}