354 lines
13 KiB
Python
354 lines
13 KiB
Python
# Source: https://github.com/Small-tailqwq/ql_script/blob/master/kuro/ql_kuro.py
|
||
# Raw: https://raw.githubusercontent.com/Small-tailqwq/ql_script/master/kuro/ql_kuro.py
|
||
# Repo: Small-tailqwq/ql_script
|
||
# Path: kuro/ql_kuro.py
|
||
# UploadedAt: 2026-05-23T15:50:58Z
|
||
# SHA256: 46013225510794598730a4bad8387ebfbcee626a372ce9a1e605ff7acbf18bac
|
||
# Category: web版/抓包
|
||
# Evidence: web/H5关键词 + cookie/token/header
|
||
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
cron: 30 8 * * *
|
||
new Env('库街区自动签到');
|
||
|
||
基于 mxyooR/Kuro-autosignin (GPL-3.0) 精简适配青龙单文件版
|
||
上游版本: a334796 (2026-04-21)
|
||
最后更新: 2026-05-23 09:00 (UTC+8)
|
||
"""
|
||
|
||
import datetime
|
||
import json
|
||
import logging
|
||
import os
|
||
import random
|
||
import socket
|
||
import time
|
||
import uuid
|
||
from typing import Dict, List, Optional
|
||
import requests
|
||
|
||
try:
|
||
from sendNotify import send
|
||
except Exception:
|
||
try:
|
||
from notify import send
|
||
except Exception:
|
||
send = None
|
||
|
||
TIMEOUT = 30
|
||
GAME_WUWA_ID = "3"
|
||
GAME_PGR_ID = "2"
|
||
SERVER_WUWA = "76402e5b20be2c39f095a152090afddc"
|
||
SERVER_PGR = "1000"
|
||
MAX_RETRIES = 3
|
||
|
||
|
||
class API:
|
||
USER_MINE = "https://api.kurobbs.com/user/mineV2"
|
||
USER_SIGN_IN = "https://api.kurobbs.com/user/signIn"
|
||
USER_ROLE_LIST = "https://api.kurobbs.com/user/role/findRoleList"
|
||
FORUM_LIST = "https://api.kurobbs.com/forum/list"
|
||
FORUM_POST_DETAIL = "https://api.kurobbs.com/forum/getPostDetail"
|
||
FORUM_LIKE = "https://api.kurobbs.com/forum/like"
|
||
TASK_PROCESS = "https://api.kurobbs.com/encourage/level/getTaskProcess"
|
||
TASK_SHARE = "https://api.kurobbs.com/encourage/level/shareTask"
|
||
GAME_SIGN_IN = "https://api.kurobbs.com/encourage/signIn/v2"
|
||
GAME_SIGN_RECORD = "https://api.kurobbs.com/encourage/signIn/queryRecordV2"
|
||
|
||
|
||
def get_ip_address() -> str:
|
||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
try:
|
||
s.connect(("8.8.8.8", 80))
|
||
return s.getsockname()[0]
|
||
except socket.error:
|
||
return "127.0.0.1"
|
||
finally:
|
||
s.close()
|
||
|
||
|
||
class KuroSigner:
|
||
def __init__(self, token_data: Dict[str, str]):
|
||
self.token = token_data['token']
|
||
self.note = token_data.get('note', '未命名账号')
|
||
|
||
self.ip = get_ip_address()
|
||
self.devcode = str(uuid.uuid4())
|
||
self.distinct_id = str(uuid.uuid4())
|
||
|
||
self.session = requests.Session()
|
||
self.user_id = None
|
||
self.wuwa_role_id = None
|
||
self.pgr_role_id = None
|
||
self.messages = []
|
||
self.success = True
|
||
|
||
def _log(self, msg: str):
|
||
logging.info(f"[{self.note}] {msg}")
|
||
self.messages.append(msg)
|
||
|
||
def _req(self, url: str, method: str = 'POST', data: Optional[dict] = None, req_type: str = 'bbs') -> dict:
|
||
headers = {
|
||
"Accept": "*/*",
|
||
"Accept-Language": "zh-CN,zh-Hans;q=0.9",
|
||
"Accept-Encoding": "gzip, deflate, br",
|
||
"Connection": "keep-alive",
|
||
"Host": "api.kurobbs.com",
|
||
"source": "ios",
|
||
}
|
||
|
||
if req_type == 'bbs':
|
||
headers.update({
|
||
"lang": "zh-Hans",
|
||
"User-Agent": "KuroGameBox/48 CFNetwork/1492.0.1 Darwin/23.3.0",
|
||
"channelId": "1",
|
||
"channel": "appstore",
|
||
"version": "2.2.0",
|
||
"model": "iPhone15,2",
|
||
"osVersion": "17.3",
|
||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||
"Cookie": f"user_token={self.token}",
|
||
"Ip": self.ip,
|
||
"distinct_id": self.distinct_id,
|
||
"devCode": self.devcode,
|
||
"token": self.token,
|
||
})
|
||
elif req_type == 'game':
|
||
headers.update({
|
||
"Accept": "application/json, text/plain, */*",
|
||
"Sec-Fetch-Site": "same-site",
|
||
"source": "ios",
|
||
"Sec-Fetch-Mode": "cors",
|
||
"Origin": "https://web-static.kurobbs.com",
|
||
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) KuroGameBox/2.2.0",
|
||
"devCode": f"{self.ip}, Mozilla/5.0 (iPhone; CPU iPhone OS 17_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) KuroGameBox/2.2.0",
|
||
"token": self.token,
|
||
})
|
||
elif req_type == 'user_info':
|
||
headers.update({
|
||
"osversion": "Android",
|
||
"countrycode": "CN",
|
||
"ip": self.ip,
|
||
"model": "2211133C",
|
||
"source": "android",
|
||
"lang": "zh-Hans",
|
||
"version": "1.0.9",
|
||
"versioncode": "1090",
|
||
"content-type": "application/x-www-form-urlencoded",
|
||
"user-agent": "okhttp/3.10.0",
|
||
"devcode": self.devcode,
|
||
"distinct_id": self.distinct_id,
|
||
"token": self.token,
|
||
})
|
||
|
||
try:
|
||
if method == 'POST':
|
||
resp = self.session.post(url, headers=headers, data=data, timeout=TIMEOUT).json()
|
||
else:
|
||
resp = self.session.get(url, headers=headers, timeout=TIMEOUT).json()
|
||
return resp
|
||
except Exception as e:
|
||
return {"code": -1, "msg": f"请求异常: {str(e)}"}
|
||
|
||
def init_user_data(self) -> bool:
|
||
res = self._req(API.USER_MINE, req_type='user_info')
|
||
if res.get('code') != 200:
|
||
self._log(f"获取用户信息失败或Token已过期: {res.get('msg')}")
|
||
self.success = False
|
||
return False
|
||
self.user_id = res['data']['mine']['userId']
|
||
|
||
res_wuwa = self._req(API.USER_ROLE_LIST, data={"gameId": GAME_WUWA_ID}, req_type='user_info')
|
||
if res_wuwa.get('code') == 200 and res_wuwa.get('data'):
|
||
self.wuwa_role_id = res_wuwa['data'][0].get('roleId')
|
||
|
||
res_pgr = self._req(API.USER_ROLE_LIST, data={"gameId": GAME_PGR_ID}, req_type='user_info')
|
||
if res_pgr.get('code') == 200 and res_pgr.get('data'):
|
||
self.pgr_role_id = res_pgr['data'][0].get('roleId')
|
||
|
||
return True
|
||
|
||
def _get_sign_reward(self, game_name: str, game_id: str, server_id: str, role_id: str) -> Optional[str]:
|
||
try:
|
||
data = {
|
||
"gameId": game_id,
|
||
"serverId": server_id,
|
||
"roleId": role_id,
|
||
"userId": self.user_id,
|
||
}
|
||
res = self._req(API.GAME_SIGN_RECORD, data=data, req_type='game')
|
||
if res.get('code') == 200 and res.get('data'):
|
||
if isinstance(res['data'], list) and len(res['data']) > 0:
|
||
return res['data'][0].get('goodsName')
|
||
return None
|
||
except Exception:
|
||
return None
|
||
|
||
def game_sign(self):
|
||
month = datetime.datetime.now().strftime("%m")
|
||
|
||
def _do_sign(game_name, game_id, server_id, role_id):
|
||
if not role_id:
|
||
self._log(f"[{game_name}] 未找到角色ID,跳过签到")
|
||
return
|
||
data = {
|
||
"gameId": game_id,
|
||
"serverId": server_id,
|
||
"roleId": role_id,
|
||
"userId": self.user_id,
|
||
"reqMonth": month,
|
||
}
|
||
res = self._req(API.GAME_SIGN_IN, data=data, req_type='game')
|
||
code = res.get('code')
|
||
|
||
if code == 200:
|
||
reward = self._get_sign_reward(game_name, game_id, server_id, role_id)
|
||
msg = f"[{game_name}] 签到成功"
|
||
if reward:
|
||
msg += f",奖励: {reward}"
|
||
self._log(msg)
|
||
elif code == 1511:
|
||
reward = self._get_sign_reward(game_name, game_id, server_id, role_id)
|
||
msg = f"[{game_name}] 今日已签到"
|
||
if reward:
|
||
msg += f",奖励: {reward}"
|
||
self._log(msg)
|
||
elif code == 1513:
|
||
self._log(f"[{game_name}] 签到失败:用户信息异常,即将禁用该账号")
|
||
self.success = False
|
||
elif code == 220:
|
||
self._log(f"[{game_name}] 签到失败:登录已过期,请重新登录")
|
||
self.success = False
|
||
else:
|
||
self._log(f"[{game_name}] 签到失败: {res.get('msg', '未知错误')} (code={code})")
|
||
raise Exception(f"{game_name} 签到失败: {res.get('msg', '未知错误')}")
|
||
|
||
_do_sign("鸣潮", GAME_WUWA_ID, SERVER_WUWA, self.wuwa_role_id)
|
||
time.sleep(1)
|
||
_do_sign("战双", GAME_PGR_ID, SERVER_PGR, self.pgr_role_id)
|
||
|
||
def forum_tasks(self):
|
||
res = self._req(API.USER_SIGN_IN, data={"gameId": "2"}, req_type='bbs')
|
||
if res.get('code') == 200:
|
||
self._log("论坛签到成功")
|
||
|
||
time.sleep(1)
|
||
|
||
res_tasks = self._req(API.TASK_PROCESS, data={"gameId": "0"}, req_type='bbs')
|
||
if res_tasks.get('code') != 200:
|
||
self._log("获取每日任务列表失败跳过")
|
||
return
|
||
|
||
tasks = res_tasks.get('data', {}).get('dailyTask', [])
|
||
tasks_map = {t.get('remark'): t.get('process') for t in tasks}
|
||
|
||
posts = []
|
||
if tasks_map.get("浏览3篇帖子", 1) == 0 or tasks_map.get("点赞5次", 1) == 0:
|
||
res_posts = self._req(API.FORUM_LIST, data={"forumId": "9", "gameId": "3", "pageIndex": "1", "pageSize": "20", "searchType": "3", "timeType": "0"}, req_type='bbs')
|
||
if res_posts.get('code') == 200:
|
||
posts = res_posts.get('data', {}).get('postList', [])
|
||
|
||
if tasks_map.get("浏览3篇帖子", 1) == 0 and posts:
|
||
view_count = 0
|
||
for p in posts[:3]:
|
||
self._req(API.FORUM_POST_DETAIL, data={"isOnlyPublisher": "0", "postId": p['postId'], "showOrderTyper": "2"}, req_type='bbs')
|
||
view_count += 1
|
||
time.sleep(random.uniform(1, 2))
|
||
self._log(f"完成浏览帖子任务 ({view_count}/3)")
|
||
|
||
if tasks_map.get("点赞5次", 1) == 0 and posts:
|
||
like_count = 0
|
||
for p in posts[:5]:
|
||
self._req(API.FORUM_LIKE, data={"forumId": 11, "gameId": 3, "likeType": 1, "operateType": 1, "postCommentId": "", "postCommentReplyId": "", "postId": p['postId'], "postType": 1, "toUserId": p['userId']}, req_type='bbs')
|
||
like_count += 1
|
||
time.sleep(random.uniform(1, 2))
|
||
self._log(f"完成点赞帖子任务 ({like_count}/5)")
|
||
|
||
if tasks_map.get("分享1次帖子", 1) == 0:
|
||
self._req(API.TASK_SHARE, data={"gameId": 3}, req_type='bbs')
|
||
self._log("完成分享帖子任务 (1/1)")
|
||
|
||
def run(self):
|
||
for attempt in range(MAX_RETRIES):
|
||
try:
|
||
if attempt > 0:
|
||
delay = random.uniform(5, 15)
|
||
self._log(f"将在 {delay:.1f} 秒后进行第 {attempt + 1}/{MAX_RETRIES} 次重试...")
|
||
time.sleep(delay)
|
||
|
||
if self.init_user_data():
|
||
self.game_sign()
|
||
self.forum_tasks()
|
||
|
||
if not self.success:
|
||
raise Exception("签到执行失败")
|
||
break
|
||
|
||
except Exception as e:
|
||
self._log(f"执行异常: {str(e)}")
|
||
self.success = False
|
||
if attempt < MAX_RETRIES - 1:
|
||
continue
|
||
else:
|
||
self._log(f"重试 {MAX_RETRIES} 次后仍然失败,放弃签到")
|
||
|
||
return "\n".join(self.messages)
|
||
|
||
|
||
def get_tokens() -> List[Dict[str, str]]:
|
||
raw = os.environ.get("KURO_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
|
||
|
||
parts = line.split('#', 1)
|
||
token_val = parts[0].strip()
|
||
note = parts[1].strip() if len(parts) > 1 else f"账号{len(token_list) + 1}"
|
||
|
||
if token_val:
|
||
token_list.append({'token': token_val, 'note': note})
|
||
|
||
return token_list
|
||
|
||
|
||
def main():
|
||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
|
||
|
||
tokens = get_tokens()
|
||
if not tokens:
|
||
logging.error("未找到 Token,请在环境变量中设置 'KURO_TOKEN'。")
|
||
return
|
||
|
||
logging.info(f"✅ 检测到 {len(tokens)} 个账号,开始执行库街区签到...\n" + "-" * 30)
|
||
|
||
notify_content = []
|
||
|
||
for token_data in tokens:
|
||
signer = KuroSigner(token_data)
|
||
result_msg = signer.run()
|
||
|
||
status_icon = "✅" if signer.success else "❌"
|
||
header = f"【{signer.note}】{status_icon}"
|
||
|
||
notify_content.append(header)
|
||
notify_content.append(result_msg)
|
||
notify_content.append("-" * 20)
|
||
|
||
final_content = "\n".join(notify_content)
|
||
logging.info(f"\n{final_content}")
|
||
|
||
if send:
|
||
send("库街区签到", final_content)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|