Files
qinglong/脚本库/微信小程序/抓包版/森空岛签到/2026-05-23_skyland_3ff34a02.py
2026-05-24 03:53:42 +00:00

436 lines
18 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Source: https://github.com/CN-Grace/QinglongScripts/blob/main/skyland.py
# Raw: https://raw.githubusercontent.com/CN-Grace/QinglongScripts/main/skyland.py
# Repo: CN-Grace/QinglongScripts
# Path: skyland.py
# UploadedAt: 2026-05-23T15:56:46Z
# SHA256: 3ff34a02f788b8d949abe98d0670868ecd5dbd849cf18490c04ca20ae3641688
# Category: 微信小程序/抓包版
# Evidence: appid/openid/session_key/encryptedData等小程序字段
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
cron: 0 8 * * *
new Env("森空岛签到")
森空岛Skland自动签到脚本
- 模拟数美设备指纹,生成设备 IDdId
- 使用用户 Token 登录森空岛,获取凭证
- 遍历绑定角色,对明日方舟、终末地等游戏进行每日签到
"""
import base64
import gzip
import hashlib
import hmac
import json
import os
import re
import time
import uuid
from urllib import parse
import requests
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers.algorithms import AES
from cryptography.hazmat.decrepit.ciphers.algorithms import TripleDES
from cryptography.hazmat.primitives.ciphers.modes import CBC, ECB
from utils import log_info, log_success, log_warning, log_error, beijing_time_str
from notifier import send as notify_send
# ==================== 用户配置 ====================
TOKENS = os.environ.get("SKYLAND_TOKEN", "").split(",")
APP_CODE = "4ca99fa6b56cc2ba"
# ==================== 数美加密常量 ====================
SM_CONFIG = {
"organization": "UWXspnCCJN4sfYlNfqps",
"appId": "default",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmxMNr7n8ZeT0tE1R9j/mPixoinPkeM+k4VGIn/s0k7N5rJAfnZ0eMER+QhwFvshzo0LNmeUkpR8uIlU/GEVr8mN28sKmwd2gpygqj0ePnBmOW4v0ZVwbSYK+izkhVFk2V/doLoMbWy6b+UnA8mkjvg0iYWRByfRsK2gdl7llqCwIDAQAB",
"protocol": "https",
"apiHost": "fp-it.portal101.cn",
}
DES_RULE = {
"appId": {"cipher": "DES", "is_encrypt": 1, "key": "uy7mzc4h", "obfuscated_name": "xx"},
"box": {"is_encrypt": 0, "obfuscated_name": "jf"},
"canvas": {"cipher": "DES", "is_encrypt": 1, "key": "snrn887t", "obfuscated_name": "yk"},
"clientSize": {"cipher": "DES", "is_encrypt": 1, "key": "cpmjjgsu", "obfuscated_name": "zx"},
"organization": {"cipher": "DES", "is_encrypt": 1, "key": "78moqjfc", "obfuscated_name": "dp"},
"os": {"cipher": "DES", "is_encrypt": 1, "key": "je6vk6t4", "obfuscated_name": "pj"},
"platform": {"cipher": "DES", "is_encrypt": 1, "key": "pakxhcd2", "obfuscated_name": "gm"},
"plugins": {"cipher": "DES", "is_encrypt": 1, "key": "v51m3pzl", "obfuscated_name": "kq"},
"pmf": {"cipher": "DES", "is_encrypt": 1, "key": "2mdeslu3", "obfuscated_name": "vw"},
"protocol": {"is_encrypt": 0, "obfuscated_name": "protocol"},
"referer": {"cipher": "DES", "is_encrypt": 1, "key": "y7bmrjlc", "obfuscated_name": "ab"},
"res": {"cipher": "DES", "is_encrypt": 1, "key": "whxqm2a7", "obfuscated_name": "hf"},
"rtype": {"cipher": "DES", "is_encrypt": 1, "key": "x8o2h2bl", "obfuscated_name": "lo"},
"sdkver": {"cipher": "DES", "is_encrypt": 1, "key": "9q3dcxp2", "obfuscated_name": "sc"},
"status": {"cipher": "DES", "is_encrypt": 1, "key": "2jbrxxw4", "obfuscated_name": "an"},
"subVersion": {"cipher": "DES", "is_encrypt": 1, "key": "eo3i2puh", "obfuscated_name": "ns"},
"svm": {"cipher": "DES", "is_encrypt": 1, "key": "fzj3kaeh", "obfuscated_name": "qr"},
"time": {"cipher": "DES", "is_encrypt": 1, "key": "q2t3odsk", "obfuscated_name": "nb"},
"timezone": {"cipher": "DES", "is_encrypt": 1, "key": "1uv05lj5", "obfuscated_name": "as"},
"tn": {"cipher": "DES", "is_encrypt": 1, "key": "x9nzj1bp", "obfuscated_name": "py"},
"trees": {"cipher": "DES", "is_encrypt": 1, "key": "acfs0xo4", "obfuscated_name": "pi"},
"ua": {"cipher": "DES", "is_encrypt": 1, "key": "k92crp1t", "obfuscated_name": "bj"},
"url": {"cipher": "DES", "is_encrypt": 1, "key": "y95hjkoo", "obfuscated_name": "cf"},
"version": {"is_encrypt": 0, "obfuscated_name": "version"},
"vpw": {"cipher": "DES", "is_encrypt": 1, "key": "r9924ab5", "obfuscated_name": "ca"},
}
BROWSER_ENV = {
"plugins": "MicrosoftEdgePDFPluginPortableDocumentFormatinternal-pdf-viewer1,MicrosoftEdgePDFViewermhjfbmdgcfjbbpaeojofohoefgiehjai1",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
"canvas": "259ffe69",
"timezone": -480,
"platform": "Win32",
"url": "https://www.skland.com/",
"referer": "",
"res": "1920_1080_24_1.25",
"clientSize": "0_0_1080_1920_1920_1080_1920_1080",
"status": "0011",
}
_PUBLIC_KEY = serialization.load_der_public_key(base64.b64decode(SM_CONFIG["publicKey"]))
_DID_CACHE = None
# ==================== 数美加密函数 ====================
def _des_encrypt(obj: dict) -> dict:
result = {}
for key, value in obj.items():
rule = DES_RULE.get(key)
if not rule:
result[key] = value
continue
if rule["is_encrypt"] == 1:
key = rule["key"].encode("utf-8")
cipher = Cipher(TripleDES(key * 3), ECB())
data = str(value).encode("utf-8")
data += b"\x00" * (8 - len(data) % 8)
encrypted = cipher.encryptor().update(data)
result[rule["obfuscated_name"]] = base64.b64encode(encrypted).decode("utf-8")
else:
result[rule["obfuscated_name"]] = value
return result
def _aes_encrypt(data: bytes, key: bytes) -> str:
iv = b"0102030405060708"
cipher = Cipher(AES(key), CBC(iv))
encryptor = cipher.encryptor()
pad_len = 16 - (len(data) % 16)
data += b"\x00" * pad_len
encrypted = encryptor.update(data) + encryptor.finalize()
return encrypted.hex()
def _gzip_compress(obj: dict) -> bytes:
json_str = json.dumps(obj, separators=(",", ":"), ensure_ascii=False)
return base64.b64encode(gzip.compress(json_str.encode("utf-8"), compresslevel=2, mtime=0))
def _get_tn(obj: dict) -> str:
sorted_keys = sorted(obj.keys())
parts = []
for k in sorted_keys:
v = obj[k]
if isinstance(v, (int, float)):
parts.append(str(int(v * 10000)))
elif isinstance(v, dict):
parts.append(_get_tn(v))
else:
parts.append(str(v))
return "".join(parts)
def _get_smid() -> str:
t = time.localtime()
time_str = f"{t.tm_year}{t.tm_mon:02d}{t.tm_mday:02d}{t.tm_hour:02d}{t.tm_min:02d}{t.tm_sec:02d}"
uid = str(uuid.uuid4())
md5_uid = hashlib.md5(uid.encode()).hexdigest()
base = time_str + md5_uid + "00"
smsk = hashlib.md5(("smsk_web_" + base).encode()).hexdigest()[:14]
return base + smsk + "0"
def get_device_id() -> str:
global _DID_CACHE
if _DID_CACHE:
log_info(f"使用缓存的设备 ID: {_DID_CACHE}")
return _DID_CACHE
log_info("开始生成设备 ID...")
uid = str(uuid.uuid4()).encode("utf-8")
pri_id = hashlib.md5(uid).hexdigest()[:16]
ep = _PUBLIC_KEY.encrypt(uid, padding.PKCS1v15())
ep_b64 = base64.b64encode(ep).decode("utf-8")
browser = BROWSER_ENV.copy()
current_time = int(time.time() * 1000)
browser.update({"vpw": str(uuid.uuid4()), "svm": current_time, "trees": str(uuid.uuid4()), "pmf": current_time})
des_target = {
**browser,
"protocol": 102, "organization": SM_CONFIG["organization"], "appId": SM_CONFIG["appId"],
"os": "web", "version": "3.0.0", "sdkver": "3.0.0",
"box": "", "rtype": "all", "smid": _get_smid(), "subVersion": "1.0.0", "time": 0,
}
des_target["tn"] = hashlib.md5(_get_tn(des_target).encode()).hexdigest()
des_encrypted = _des_encrypt(des_target)
compressed = _gzip_compress(des_encrypted)
aes_encrypted = _aes_encrypt(compressed, pri_id.encode("utf-8"))
url = f"https://{SM_CONFIG['apiHost']}/deviceprofile/v4"
payload = {"appId": "default", "compress": 2, "data": aes_encrypted, "encode": 5, "ep": ep_b64, "organization": SM_CONFIG["organization"], "os": "web"}
log_info("向数美服务请求设备 ID...")
resp = requests.post(url, json=payload).json()
if resp.get("code") != 1100:
raise RuntimeError(f"获取设备 ID 失败: {resp}")
_DID_CACHE = "B" + resp["detail"]["deviceId"]
log_success(f"设备 ID 生成成功: {_DID_CACHE}")
return _DID_CACHE
# ==================== 签到相关函数 ====================
def generate_signature(path: str, body_or_query: str, token: str, d_id: str) -> tuple:
timestamp = str(int(time.time()) - 2)
header_ca = {"platform": "3", "timestamp": timestamp, "dId": d_id, "vName": "1.0.0"}
header_str = json.dumps(header_ca, separators=(",", ":"))
raw_string = path + body_or_query + timestamp + header_str
hmac_sha256 = hmac.new(token.encode(), raw_string.encode(), hashlib.sha256).hexdigest()
sign = hashlib.md5(hmac_sha256.encode()).hexdigest()
return sign, header_ca
def get_sign_headers(url: str, method: str, body, token: str, d_id: str, cred: str) -> dict:
parsed = parse.urlparse(url)
if method.lower() == "get":
sign, header_ca = generate_signature(parsed.path, parsed.query, token, d_id)
else:
body_str = json.dumps(body) if body else ""
sign, header_ca = generate_signature(parsed.path, body_str, token, d_id)
return {
"cred": cred,
"User-Agent": "Mozilla/5.0 (Linux; Android 12; SM-A5560 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.61 Safari/537.36; SKLand/1.52.1",
"Accept-Encoding": "gzip", "Connection": "close",
"X-Requested-With": "com.hypergryph.skland",
"sign": sign, **header_ca,
}
def get_grant_code(token: str, d_id: str) -> str:
url = "https://as.hypergryph.com/user/oauth2/v2/grant"
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 12; SM-A5560 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.61 Safari/537.36; SKLand/1.52.1",
"Accept-Encoding": "gzip", "Connection": "close",
"dId": d_id, "X-Requested-With": "com.hypergryph.skland",
}
payload = {"appCode": APP_CODE, "token": token, "type": 0}
log_info("正在获取 grant code...")
resp = requests.post(url, json=payload, headers=headers).json()
if resp.get("status") != 0:
raise RuntimeError(f"获取 grant code 失败: {resp.get('msg')}")
code = resp["data"]["code"]
log_info(f"grant code 获取成功: {code[:10]}...")
return code
def get_cred(grant_code: str, d_id: str) -> dict:
url = "https://zonai.skland.com/web/v1/user/auth/generate_cred_by_code"
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 12; SM-A5560 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.61 Safari/537.36; SKLand/1.52.1",
"Accept-Encoding": "gzip", "Connection": "close",
"dId": d_id, "X-Requested-With": "com.hypergryph.skland",
}
payload = {"code": grant_code, "kind": 1}
log_info("正在获取 cred...")
resp = requests.post(url, json=payload, headers=headers).json()
if resp.get("code") != 0:
raise RuntimeError(f"获取 cred 失败: {resp.get('message')}")
cred_data = resp["data"]
log_info(f"cred 获取成功token: {cred_data['token'][:10]}...")
return cred_data
def get_binding_list(cred: str, token: str, d_id: str) -> list:
url = "https://zonai.skland.com/api/v1/game/player/binding"
headers = get_sign_headers(url, "get", None, token, d_id, cred)
log_info("正在获取绑定角色列表...")
resp = requests.get(url, headers=headers).json()
if resp.get("code") != 0:
if resp.get("message") == "用户未登录":
log_error("用户登录失效,请重新获取 token")
else:
log_error(f"获取角色列表失败: {resp.get('message')}")
return []
characters = []
for game in resp["data"]["list"]:
if game.get("appCode") not in ("arknights", "endfield"):
continue
for char in game.get("bindingList", []):
char["appCode"] = game["appCode"]
characters.append(char)
log_info(f"获取到 {len(characters)} 个可签到角色")
return characters
def sign_arknights(character: dict, cred: str, token: str, d_id: str) -> list:
url = "https://zonai.skland.com/api/v1/game/attendance"
body = {"gameId": character.get("gameId"), "uid": character.get("uid")}
headers = get_sign_headers(url, "post", body, token, d_id, cred)
log_info(f"正在为角色 {character.get('nickName')} 进行明日方舟签到...")
resp = requests.post(url, headers=headers, json=body).json()
game_name = character.get("gameName", "明日方舟")
channel = character.get("channelName", "")
nickname = character.get("nickName") or ""
if resp.get("code") != 0:
msg = f"[{game_name}] 角色 {nickname}({channel}) 签到失败: {resp.get('message')}"
log_warning(msg)
return [f"{msg}"]
awards = resp["data"]["awards"]
award_text = "".join([f"{a['resource']['name']}x{a.get('count', 1)}" for a in awards])
msg = f"[{game_name}] 角色 {nickname}({channel}) 签到成功,获得 {award_text}"
log_success(msg)
return [f"{msg}"]
def do_sign_endfield(role: dict, cred: str, token: str, d_id: str) -> requests.Response:
url = "https://zonai.skland.com/web/v1/game/endfield/attendance"
headers = get_sign_headers(url, "post", None, token, d_id, cred)
headers.update({
"Content-Type": "application/json",
"sk-game-role": f"3_{role['roleId']}_{role['serverId']}",
"referer": "https://game.skland.com/",
"origin": "https://game.skland.com/",
})
return requests.post(url, headers=headers)
def sign_endfield(character: dict, cred: str, token: str, d_id: str) -> list:
roles = character.get("roles", [])
game_name = character.get("gameName", "终末地")
channel = character.get("channelName", "")
results = []
for role in roles:
nickname = role.get("nickname") or ""
log_info(f"正在为角色 {nickname} 进行终末地签到...")
resp = do_sign_endfield(role, cred, token, d_id)
data = resp.json()
if data.get("code") != 0:
msg = f"[{game_name}] 角色 {nickname}({channel}) 签到失败: {data.get('message')}"
log_warning(msg)
results.append(f"{msg}")
else:
awards = []
info_map = data["data"]["resourceInfoMap"]
for award_id in data["data"]["awardIds"]:
aid = award_id["id"]
awards.append(f"{info_map[aid]['name']}x{info_map[aid]['count']}")
msg = f"[{game_name}] 角色 {nickname}({channel}) 签到成功,获得 {','.join(awards)}"
log_success(msg)
results.append(f"{msg}")
return results
def process_token(user_token: str, d_id: str, account_index: int) -> list:
logs = []
try:
log_info(f"===== 开始处理第 {account_index} 个账号 =====")
grant_code = get_grant_code(user_token, d_id)
cred_data = get_cred(grant_code, d_id)
cred = cred_data["cred"]
token = cred_data["token"]
characters = get_binding_list(cred, token, d_id)
if not characters:
logs.append(" 该账号下无绑定角色或无支持签到的游戏")
return logs
for char in characters:
if char["appCode"] == "arknights":
logs.extend(sign_arknights(char, cred, token, d_id))
elif char["appCode"] == "endfield":
logs.extend(sign_endfield(char, cred, token, d_id))
log_info(f"===== 第 {account_index} 个账号处理完成 =====")
except Exception as e:
err_msg = f"处理账号 {user_token[:8]}... 时出错: {e}"
log_error(err_msg)
logs.append(f"{err_msg}")
return logs
def parse_log_line(log: str):
pattern = r'^(✅|❌|)\s*\[(.+?)\]\s*角色\s+(.+?)\s*\((.+?)\)\s*签到(成功|失败)[:]?\s*(.*)$'
match = re.match(pattern, log)
if match:
emoji, game, role, channel, status, extra = match.groups()
return {"emoji": emoji, "game": game, "role": role, "channel": channel, "status": status, "extra": extra.strip()}
return None
def format_push_message(accounts_logs):
lines = ["📋 森空岛签到服务报告", f"执行时间: {beijing_time_str()}", ""]
for acc_idx, logs in enumerate(accounts_logs, 1):
game_dict = {}
info_lines = []
for log in logs:
parsed = parse_log_line(log)
if parsed:
game = parsed["game"]
if game not in game_dict:
game_dict[game] = []
game_dict[game].append(parsed)
else:
if log.strip():
info_lines.append(f" {log}")
if info_lines:
lines.extend(info_lines)
lines.append("")
for game, roles in game_dict.items():
lines.append(f" {game}")
for role_info in roles:
emoji = role_info["emoji"]
status = role_info["status"]
extra = role_info["extra"]
if emoji == "":
lines.append(f" 📝 签到: ✅ 成功")
if extra:
lines.append(f" 🎁 奖励: {extra}")
else:
lines.append(f" 📝 签到: ❌ 失败")
if extra:
lines.append(f" 原因: {extra}")
lines.append("")
return "\n".join(lines).rstrip()
def main():
if not TOKENS or TOKENS == [""]:
log_error("未设置 SKYLAND_TOKEN 环境变量,请设置后运行")
return
try:
d_id = get_device_id()
except Exception as e:
log_error(f"获取设备 ID 失败,无法继续: {e}")
return
all_accounts_logs = []
for idx, token in enumerate(TOKENS, 1):
token = token.strip()
if not token:
continue
logs = process_token(token, d_id, idx)
all_accounts_logs.append(logs)
push_content = format_push_message(all_accounts_logs)
print("\n" + push_content + "\n")
notify_send("森空岛 签到报告", push_content)
if __name__ == "__main__":
main()