Collect authenticated Qinglong scripts batch
This commit is contained in:
514
脚本库/微信小程序/抓包版/森空岛自动签到/2026-03-02_ql_skyland_4e8e3ff9.py
Normal file
514
脚本库/微信小程序/抓包版/森空岛自动签到/2026-03-02_ql_skyland_4e8e3ff9.py
Normal file
@@ -0,0 +1,514 @@
|
||||
# Source: https://github.com/Small-tailqwq/ql_script/blob/master/skyland/ql_skyland.py
|
||||
# Raw: https://raw.githubusercontent.com/Small-tailqwq/ql_script/master/skyland/ql_skyland.py
|
||||
# Repo: Small-tailqwq/ql_script
|
||||
# Path: skyland/ql_skyland.py
|
||||
# UploadedAt: 2026-03-02T11:07:04Z
|
||||
# SHA256: 4e8e3ff95ea19345dea4a39e3a90f5ae71eadf782bf338e14bd487dc8ad6e805
|
||||
# Category: 微信小程序/抓包版
|
||||
# Evidence: appid/openid/session_key/encryptedData等小程序字段
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
cron: 0 8 * * *
|
||||
new Env('森空岛自动签到');
|
||||
"""
|
||||
|
||||
import base64
|
||||
import gzip
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from urllib import parse
|
||||
|
||||
import requests
|
||||
|
||||
# 尝试导入 cryptography,这是森空岛加密算法必须的库
|
||||
try:
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.decrepit.ciphers.algorithms import TripleDES
|
||||
except ImportError:
|
||||
print("错误: 缺少依赖 'cryptography'。请在青龙面板-依赖管理-Python3中安装 'cryptography'。")
|
||||
exit(1)
|
||||
|
||||
# 尝试导入青龙通知
|
||||
try:
|
||||
from sendNotify import send # type: ignore
|
||||
except Exception:
|
||||
try:
|
||||
from notify import send # type: ignore
|
||||
except Exception:
|
||||
send = None
|
||||
|
||||
# ================= 配置常量 =================
|
||||
DEFAULT_TIMEOUT = 30
|
||||
APP_CODE = '4ca99fa6b56cc2ba'
|
||||
DEVICES_INFO_URL = "https://fp-it.portal101.cn/deviceprofile/v4"
|
||||
LOGIN_CODE_URL = "https://as.hypergryph.com/general/v1/send_phone_code"
|
||||
TOKEN_PHONE_CODE_URL = "https://as.hypergryph.com/user/auth/v2/token_by_phone_code"
|
||||
TOKEN_PASSWORD_URL = "https://as.hypergryph.com/user/auth/v1/token_by_phone_password"
|
||||
GRANT_CODE_URL = "https://as.hypergryph.com/user/oauth2/v2/grant"
|
||||
CRED_CODE_URL = "https://zonai.skland.com/web/v1/user/auth/generate_cred_by_code"
|
||||
BINDING_URL = "https://zonai.skland.com/api/v1/game/player/binding"
|
||||
SIGN_URL_MAPPING = {
|
||||
'arknights': 'https://zonai.skland.com/api/v1/game/attendance',
|
||||
'endfield': 'https://zonai.skland.com/web/v1/game/endfield/attendance'
|
||||
}
|
||||
|
||||
# ================= 数美(SecuritySm) 加密逻辑整合 =================
|
||||
# 原作者: xxyz30 / Modified for QingLong
|
||||
class SecurityUtils:
|
||||
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',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _des_encrypt(o: dict) -> dict:
|
||||
result = {}
|
||||
for i in o.keys():
|
||||
if i in SecurityUtils.DES_RULE.keys():
|
||||
rule = SecurityUtils.DES_RULE[i]
|
||||
res = o[i]
|
||||
if rule.get('is_encrypt') == 1:
|
||||
c = Cipher(TripleDES(rule['key'].encode('utf-8')), modes.ECB())
|
||||
data = str(res).encode('utf-8')
|
||||
# PKCS5/PKCS7 padding equivalent logic manually implemented or handled
|
||||
# Manual zero padding as per original code (though original said \x00*8, TripleDES block is 8)
|
||||
pad_len = 8 - (len(data) % 8)
|
||||
data += b'\x00' * pad_len
|
||||
encryptor = c.encryptor()
|
||||
encrypted_bytes = encryptor.update(data) + encryptor.finalize()
|
||||
res = base64.b64encode(encrypted_bytes).decode('utf-8')
|
||||
result[rule['obfuscated_name']] = res
|
||||
else:
|
||||
result[i] = o[i]
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _aes_encrypt(v: bytes, k: bytes) -> str:
|
||||
iv = '0102030405060708'
|
||||
c = Cipher(algorithms.AES(k), modes.CBC(iv.encode('utf-8')))
|
||||
# Manual Zero padding logic from original code
|
||||
v_padded = v + b'\x00'
|
||||
while len(v_padded) % 16 != 0:
|
||||
v_padded += b'\x00'
|
||||
encryptor = c.encryptor()
|
||||
return (encryptor.update(v_padded) + encryptor.finalize()).hex()
|
||||
|
||||
@staticmethod
|
||||
def _gzip_compress(o: dict) -> bytes:
|
||||
json_str = json.dumps(o, ensure_ascii=False)
|
||||
return gzip.compress(json_str.encode('utf-8'), 2, mtime=0)
|
||||
|
||||
@staticmethod
|
||||
def _get_tn(o: dict) -> str:
|
||||
sorted_keys = sorted(o.keys())
|
||||
result_list = []
|
||||
for i in sorted_keys:
|
||||
v = o[i]
|
||||
if isinstance(v, (int, float)):
|
||||
v = str(v * 10000)
|
||||
elif isinstance(v, dict):
|
||||
v = SecurityUtils._get_tn(v)
|
||||
result_list.append(str(v))
|
||||
return ''.join(result_list)
|
||||
|
||||
@staticmethod
|
||||
def _get_smid() -> str:
|
||||
t = time.localtime()
|
||||
_time = '{}{:0>2d}{:0>2d}{:0>2d}{:0>2d}{:0>2d}'.format(
|
||||
t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec)
|
||||
uid = str(uuid.uuid4())
|
||||
v = _time + hashlib.md5(uid.encode('utf-8')).hexdigest() + '00'
|
||||
smsk_web = hashlib.md5(('smsk_web_' + v).encode('utf-8')).hexdigest()[0:14]
|
||||
return v + smsk_web + '0'
|
||||
|
||||
@classmethod
|
||||
def get_d_id(cls) -> str:
|
||||
try:
|
||||
pk = serialization.load_der_public_key(base64.b64decode(cls.SM_CONFIG['publicKey']))
|
||||
uid = str(uuid.uuid4()).encode('utf-8')
|
||||
pri_id = hashlib.md5(uid).hexdigest()[0:16]
|
||||
|
||||
ep = pk.encrypt(uid, padding.PKCS1v15())
|
||||
ep_str = base64.b64encode(ep).decode('utf-8')
|
||||
|
||||
browser = cls.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': cls.SM_CONFIG['organization'],
|
||||
'appId': cls.SM_CONFIG['appId'],
|
||||
'os': 'web',
|
||||
'version': '3.0.0',
|
||||
'sdkver': '3.0.0',
|
||||
'box': '',
|
||||
'rtype': 'all',
|
||||
'smid': cls._get_smid(),
|
||||
'subVersion': '1.0.0',
|
||||
'time': 0
|
||||
}
|
||||
des_target['tn'] = hashlib.md5(cls._get_tn(des_target).encode()).hexdigest()
|
||||
|
||||
# GZIP -> Base64(implicitly handled in _AES logic in original code? No, original returns bytes)
|
||||
# Original code: _AES(GZIP(_DES(des_target)), priId...)
|
||||
# Note: The original _DES returns a dict. _GZIP takes a dict and returns b64 encoded bytes inside gzip stream.
|
||||
|
||||
des_output = cls._des_encrypt(des_target)
|
||||
gzip_output = base64.b64encode(cls._gzip_compress(des_output)) # Original GZIP function returns b64 encoded
|
||||
|
||||
# Original code passes the result of GZIP (which is b64 bytes) to AES
|
||||
des_result = cls._aes_encrypt(gzip_output, pri_id.encode('utf-8'))
|
||||
|
||||
response = requests.post(DEVICES_INFO_URL, json={
|
||||
'appId': 'default',
|
||||
'compress': 2,
|
||||
'data': des_result,
|
||||
'encode': 5,
|
||||
'ep': ep_str,
|
||||
'organization': cls.SM_CONFIG['organization'],
|
||||
'os': 'web'
|
||||
}, timeout=10)
|
||||
|
||||
resp = response.json()
|
||||
if resp['code'] != 1100:
|
||||
logging.error(f"数美设备注册失败: {resp}")
|
||||
return 'B' + str(uuid.uuid4()) # Fallback
|
||||
|
||||
return 'B' + resp['detail']['deviceId']
|
||||
except Exception as e:
|
||||
logging.error(f"计算dId时发生错误: {e}")
|
||||
# Fallback random UUID if calculation fails
|
||||
return 'B' + str(uuid.uuid4())
|
||||
|
||||
# ================= 业务逻辑 =================
|
||||
|
||||
@dataclass
|
||||
class SignResult:
|
||||
account_mask: str
|
||||
success: bool
|
||||
messages: List[str]
|
||||
|
||||
class SkylandSigner:
|
||||
def __init__(self, token_data: Dict[str, str]):
|
||||
# 接收字典:{'token': '...', 'note': '...'}
|
||||
self.raw_token = token_data['token']
|
||||
self.note = token_data.get('note')
|
||||
self.user_token = self._parse_token(self.raw_token)
|
||||
# ... (其他初始化代码保持不变: self.cred, self.d_id, self.session 等)
|
||||
self.cred: str = ""
|
||||
self.session_token: str = ""
|
||||
self.d_id = SecurityUtils.get_d_id()
|
||||
self.session = requests.Session()
|
||||
self.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': self.d_id,
|
||||
'X-Requested-With': 'com.hypergryph.skland'
|
||||
}
|
||||
self.sign_headers_base = {
|
||||
'platform': '3',
|
||||
'timestamp': '',
|
||||
'dId': self.d_id,
|
||||
'vName': '1.0.0'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _parse_token(raw_token: str) -> str:
|
||||
"""兼容 JSON 格式或纯字符串格式的 Token"""
|
||||
try:
|
||||
t = json.loads(raw_token)
|
||||
return t['data']['content']
|
||||
except:
|
||||
return raw_token.strip()
|
||||
|
||||
def _generate_signature(self, path: str, body_or_query: str):
|
||||
"""生成 HMAC-SHA256 + MD5 签名"""
|
||||
# 时间戳 -2 秒以避免服务器判定时间过快
|
||||
t = str(int(time.time()) - 2)
|
||||
# 注意:这里使用的是 cred 接口换取到的临时 session_token 进行签名
|
||||
token_bytes = self.session_token.encode('utf-8')
|
||||
|
||||
header_ca = self.sign_headers_base.copy()
|
||||
header_ca['timestamp'] = t
|
||||
header_ca_str = json.dumps(header_ca, separators=(',', ':'))
|
||||
|
||||
s = path + body_or_query + t + header_ca_str
|
||||
hex_s = hmac.new(token_bytes, s.encode('utf-8'), hashlib.sha256).hexdigest()
|
||||
md5_sign = hashlib.md5(hex_s.encode('utf-8')).hexdigest()
|
||||
|
||||
return md5_sign, header_ca
|
||||
|
||||
def _get_sign_headers(self, url: str, method: str, body: Optional[dict]) -> dict:
|
||||
h = self.headers.copy()
|
||||
h['cred'] = self.cred
|
||||
|
||||
p = parse.urlparse(url)
|
||||
content = ""
|
||||
if method.lower() == 'get':
|
||||
content = p.query
|
||||
else:
|
||||
content = json.dumps(body) if body is not None else ""
|
||||
|
||||
sign, header_ca = self._generate_signature(p.path, content)
|
||||
h['sign'] = sign
|
||||
for k, v in header_ca.items():
|
||||
h[k] = v
|
||||
return h
|
||||
|
||||
def login(self):
|
||||
"""使用 Hypergryph Token 换取 Cred 和 Session Token"""
|
||||
# 1. Get Grant Code
|
||||
resp_grant = self.session.post(GRANT_CODE_URL, json={
|
||||
'appCode': APP_CODE,
|
||||
'token': self.user_token,
|
||||
'type': 0
|
||||
}, headers=self.headers).json()
|
||||
|
||||
if resp_grant.get('status') != 0:
|
||||
raise ValueError(f"获取授权代码失败: {resp_grant.get('msg')}")
|
||||
grant_code = resp_grant['data']['code']
|
||||
|
||||
# 2. Get Cred
|
||||
resp_cred = self.session.post(CRED_CODE_URL, json={
|
||||
'code': grant_code,
|
||||
'kind': 1
|
||||
}, headers=self.headers).json()
|
||||
|
||||
if resp_cred.get('code') != 0:
|
||||
raise ValueError(f"获取Cred失败: {resp_cred.get('message')}")
|
||||
|
||||
self.cred = resp_cred['data']['cred']
|
||||
self.session_token = resp_cred['data']['token'] # 重要:用于签名
|
||||
|
||||
def get_bindings(self) -> List[dict]:
|
||||
headers = self._get_sign_headers(BINDING_URL, 'get', None)
|
||||
resp = self.session.get(BINDING_URL, headers=headers).json()
|
||||
|
||||
if resp['code'] != 0:
|
||||
raise ValueError(f"获取绑定角色失败: {resp.get('message')}")
|
||||
|
||||
bindings = []
|
||||
for app in resp['data']['list']:
|
||||
if app.get('appCode') not in SIGN_URL_MAPPING:
|
||||
continue
|
||||
for char in app.get('bindingList'):
|
||||
char['appCode'] = app['appCode']
|
||||
bindings.append(char)
|
||||
return bindings
|
||||
|
||||
def sign_character(self, character: dict) -> str:
|
||||
app_code = character['appCode']
|
||||
url = SIGN_URL_MAPPING[app_code]
|
||||
|
||||
# 构造请求体
|
||||
body = {}
|
||||
if app_code == 'arknights':
|
||||
body = {'gameId': character.get('gameId'), 'uid': character.get('uid')}
|
||||
elif app_code == 'endfield':
|
||||
# 终末地通过 sk-game-role 头传递角色信息,Body为空? 参考原代码
|
||||
# 原代码 endfield 需要特殊处理 headers
|
||||
pass
|
||||
|
||||
# 签名
|
||||
headers = self._get_sign_headers(url, 'post', body if app_code != 'endfield' else None)
|
||||
|
||||
# 终末地特殊处理
|
||||
if app_code == 'endfield':
|
||||
roles = character.get('roles', [])
|
||||
msgs = []
|
||||
for role in roles:
|
||||
headers['Content-Type'] = 'application/json'
|
||||
headers['sk-game-role'] = f'3_{role["roleId"]}_{role["serverId"]}'
|
||||
headers['referer'] = 'https://game.skland.com/'
|
||||
headers['origin'] = 'https://game.skland.com/'
|
||||
|
||||
r = self.session.post(url, headers=headers)
|
||||
rj = r.json()
|
||||
nickname = role.get('nickname', '未知')
|
||||
if rj.get('code') != 0:
|
||||
msgs.append(f"[终末地]{nickname}: {rj.get('message')}")
|
||||
else:
|
||||
msgs.append(f"[终末地]{nickname}: 签到成功")
|
||||
return "\n".join(msgs)
|
||||
|
||||
# 明日方舟
|
||||
resp = self.session.post(url, headers=headers, json=body).json()
|
||||
|
||||
name = character.get('nickName', '未知')
|
||||
channel = character.get('channelName', '')
|
||||
game_title = "明日方舟" if app_code == 'arknights' else app_code
|
||||
|
||||
if resp.get('code') != 0:
|
||||
return f"[{game_title}] {name}({channel}): 失败 - {resp.get('message')}"
|
||||
|
||||
awards = resp['data']['awards']
|
||||
award_str = ",".join([f"{j['resource']['name']}x{j.get('count', 1)}" for j in awards])
|
||||
return f"[{game_title}] {name}({channel}): 成功 - 获得 {award_str}"
|
||||
|
||||
def run(self) -> SignResult:
|
||||
# 如果有备注,直接用备注;否则用 Token 掩码
|
||||
if self.note:
|
||||
account_display = self.note
|
||||
else:
|
||||
account_display = self.user_token[:5] + "***" + self.user_token[-5:]
|
||||
|
||||
logs = []
|
||||
try:
|
||||
self.login()
|
||||
bindings = self.get_bindings()
|
||||
if not bindings:
|
||||
return SignResult(account_display, False, ["未找到绑定的角色"])
|
||||
|
||||
all_success = True
|
||||
for char in bindings:
|
||||
try:
|
||||
msg = self.sign_character(char)
|
||||
logs.append(msg)
|
||||
if "失败" in msg:
|
||||
all_success = False
|
||||
except Exception as e:
|
||||
logs.append(f"角色签到异常: {str(e)}")
|
||||
all_success = False
|
||||
|
||||
return SignResult(account_display, all_success, logs)
|
||||
|
||||
except Exception as e:
|
||||
return SignResult(account_display, False, [f"账号执行异常: {str(e)}"])
|
||||
|
||||
# ================= 主程序入口 =================
|
||||
|
||||
def get_tokens() -> List[Dict[str, str]]:
|
||||
"""
|
||||
从环境变量读取 Token,支持以下格式:
|
||||
1. 纯 Token
|
||||
2. Token # 备注 (推荐)
|
||||
支持通过 换行 或 & 或 , 分割多个账号
|
||||
"""
|
||||
raw = os.environ.get("SK_TOKEN", os.environ.get("TOKEN", "")).strip()
|
||||
if not raw:
|
||||
return []
|
||||
|
||||
# 预处理:将 & 和 , 统一替换为换行符,以便按行处理
|
||||
# 这样可以同时兼容一行写多个(用&分割)和多行写多个
|
||||
raw = raw.replace('&', '\n').replace(',', '\n')
|
||||
|
||||
token_list = []
|
||||
for line in raw.split('\n'):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# 拆分 Token 和 备注
|
||||
parts = line.split('#', 1)
|
||||
token_val = parts[0].strip()
|
||||
note = parts[1].strip() if len(parts) > 1 else None
|
||||
|
||||
if token_val:
|
||||
token_list.append({
|
||||
'token': token_val,
|
||||
'note': note
|
||||
})
|
||||
|
||||
return token_list
|
||||
|
||||
def main():
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
tokens = get_tokens() # 这里返回的是 [{'token':..., 'note':...}, ...]
|
||||
if not tokens:
|
||||
logging.error("未找到 Token。请设置环境变量 'SK_TOKEN'。")
|
||||
return
|
||||
|
||||
logging.info(f"检测到 {len(tokens)} 个账号,开始执行...")
|
||||
|
||||
notify_content = []
|
||||
|
||||
for idx, token_data in enumerate(tokens):
|
||||
# 显示备注名,体验更好
|
||||
name_display = token_data['note'] if token_data['note'] else f"账号{idx + 1}"
|
||||
logging.info(f"--- 正在处理: {name_display} ---")
|
||||
|
||||
signer = SkylandSigner(token_data) # 传入整个字典
|
||||
result = signer.run()
|
||||
|
||||
# 构建日志和通知
|
||||
status_icon = "✅" if result.success else "❌"
|
||||
header = f"账号: {result.account_mask} {status_icon}"
|
||||
logging.info(header)
|
||||
for msg in result.messages:
|
||||
logging.info(msg)
|
||||
|
||||
notify_content.append(header)
|
||||
notify_content.extend(result.messages)
|
||||
notify_content.append("")
|
||||
|
||||
# 推送通知
|
||||
final_content = "\n".join(notify_content)
|
||||
if send:
|
||||
send("森空岛自动签到", final_content)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user