514 lines
21 KiB
Python
514 lines
21 KiB
Python
# 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() |