# Source: https://github.com/LinYuanovo/AutoTaskScripts/blob/main/app/%E6%AC%A1%E5%85%83%E5%A7%AC%E5%B0%8F%E8%AF%B4.py # Raw: https://raw.githubusercontent.com/LinYuanovo/AutoTaskScripts/main/app/%E6%AC%A1%E5%85%83%E5%A7%AC%E5%B0%8F%E8%AF%B4.py # Repo: LinYuanovo/AutoTaskScripts # Path: app/次元姬小说.py # UploadedAt: 2025-08-27T15:31:08+08:00 # SHA256: d9d5f110790abe6c16dad9d7b17c22b51314611f9430b5eb988f21cd45158305 # Category: web版/抓包 # Evidence: web/H5关键词 + cookie/token/header """ 作者: 临渊 日期: 2025/6/27 name: 次元姬小说 入口: APP 功能: 签到、阅读、领取奖励 (阅读需要模拟真实时间,所以脚本会运行比较长时间,预计一个小时) 变量: cyj_account = 'deviceno&token' (api.hwnovel.com下请求的deviceno和token,用&分割) 定时: 一天一到两次 cron: 10 8,15 * * * ------------更新日志------------ 2025/6/27 V1.0 初始化脚本 2025/6/28 V1.1 优化阅读逻辑 增加阅读时间判断,如果已阅读时间超过50分钟,则跳过模拟阅读 2025/6/28 V1.2 增加自动订阅 需要填写bookId和从第多少章开始订阅 2025/7/28 V1.3 修改头部注释,以便拉库 """ MULTI_ACCOUNT_SPLIT = ["\n", "@"] # 多账户分隔符列表 NOTIFY = False # 是否推送日志,默认不推送,True则推送 AUTO_ORDER_BOOK_ID = "" # 自动订阅的小说bookId,填了则自动订阅,否则不订阅,只有第一个号会进行订阅 AUTO_ORDER_BOOK_CHAPTER = 1 # 从第多少章开始自动订阅,默认使用所有代币往后进行订阅 import os import random import time import json import logging import traceback import requests import base64 import hashlib import threading import uuid as uuidlib from Crypto.Cipher import DES from Crypto.Util.Padding import pad class AutoTask: def __init__(self, script_name): self.script_name = script_name self.log_msgs = [] self.local = threading.local() self.base_url = "https://api.hwnovel.com/api/ciyuanji/client/" self.setup_logging() def setup_logging(self): logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s\t- %(message)s', datefmt='%Y-%m-%d %H:%M:%S', handlers=[logging.StreamHandler()] ) def log(self, msg, level="info", push=True): account_prefix = getattr(self.local, "account_prefix", "") msg = f"{account_prefix}{msg}" if level == "info": logging.info(msg) elif level == "error": logging.error(msg) elif level == "warning": logging.warning(msg) if push: self.log_msgs.append(msg) def check_env(self): """ 检查环境变量 :return: 环境变量字符串 """ try: # 从环境变量获取cookie cyj_account = os.getenv(f"cyj_account") if not cyj_account: self.log("[检查环境变量]没有找到环境变量cyj_account,请检查环境变量", level="error") return None # 自动检测分隔符 split_char = None for sep in MULTI_ACCOUNT_SPLIT: if sep in cyj_account: split_char = sep break if not split_char: # 如果都没有分隔符,默认当作单账号 cyj_accounts = [cyj_account] else: cyj_accounts = cyj_account.split(split_char) for cyj_account in cyj_accounts: if "=" in cyj_account: cyj_account = cyj_account.split("=")[1] deviceno, token = cyj_account.split("&") yield deviceno, token else: deviceno, token = cyj_account.split("&") yield deviceno, token except Exception as e: self.log(f"[检查环境变量]发生错误: {str(e)}\n{traceback.format_exc()}", level="error") raise def encrypt(self, data): key = b"ZUreQN0E" cipher = DES.new(key, DES.MODE_ECB) padded_data = pad(data.encode("utf-8"), DES.block_size) encrypted = cipher.encrypt(padded_data) return base64.b64encode(encrypted).decode("utf-8") def base64_encode(self, s): return base64.b64encode(s.encode("utf-8")).decode("utf-8") def digest(self, s, method="md5"): if method == "md5": return hashlib.md5(s.encode("utf-8")).hexdigest() raise NotImplementedError("只支持md5") def uuid(self): return str(uuidlib.uuid4()) def request(self, method, url, headers, data=None): timestamp = int(time.time() * 1000) request_id = self.uuid() param = self.encrypt(json.dumps({**(data or {}), "timestamp": timestamp})) sign_str = f"param={param}&requestId={request_id}×tamp={timestamp}&key=NpkTYvpvhJjEog8Y051gQDHmReY54z5t3F0zSd9QEFuxWGqfC8g8Y4GPuabq0KPdxArlji4dSnnHCARHnkqYBLu7iIw55ibTo18" sign = self.digest(self.base64_encode(sign_str), "md5").upper() try: if method == "GET": resp = requests.get( self.base_url + url, params={ "param": param, "requestId": request_id, "sign": sign, "timestamp": timestamp }, headers=headers, timeout=10 ) else: resp = requests.post( self.base_url + url, json={ "param": param, "requestId": request_id, "sign": sign, "timestamp": timestamp }, headers=headers, timeout=10 ) resp.raise_for_status() return resp.json() except Exception as e: self.log(f"请求异常: {url} {str(e)}\n{traceback.format_exc()}", level="error") return None def signin(self, headers): """ 每日签到 """ response = self.request("POST", "sign/sign", headers, {}) if response["code"] == "200": self.log(f"签到: {response['msg']}") return response else: self.log(f"签到: {response['msg']}", level="warning") return False def get_task_list(self, headers): """ 获取任务列表 :return: 任务列表 """ response = self.request("GET", "task/getTaskList", headers, {}) if response["code"] == "200": task_list = response["data"]["daliyTask"] return task_list else: self.log(f"获取任务列表: {response['msg']}", level="warning") return False def read_start(self, headers): """ 开始阅读 """ data = { "startChapter": "3632603", "startChapterName": "第1章 邪恶app", "bookName": "人渣男主,为所欲为", "bookId": "14827", "timestamp": int(time.time() * 1000) } response = self.request("POST", "read/start", headers, data) if response["code"] == "200": readId = response["data"]["readId"] # self.log(f"开始阅读: readId={readId}", level="info") return readId else: self.log(f"开始阅读: {response['msg']}", level="error") return False def read_start_chapter(self, headers, readId): """ 阅读章节 :param readId: 阅读ID """ data = { "isBuy": "0", "isFee": "0", "readId": readId, "chapterId": "3632603", "chapterName": "第1章 邪恶app", "bookName": "人渣男主,为所欲为", "bookId": "14827", "timestamp": int(time.time() * 1000) } response = self.request("POST", "read/startChapter", headers, data) if response["code"] == "200": readChapterId = response["data"]["readChapterId"] # self.log(f"阅读章节: readChapterId={readChapterId}", level="info") return readChapterId else: self.log(f"阅读章节: {response['msg']}", level="error") return False def read_end(self, headers, readId): """ 结束阅读 :param readId: 阅读ID """ data = { "endChapterName": "第1章 邪恶app", "readId": readId, "chapterCount": "1", "endChapter": "3632603", "bookId": "14827", "timestamp": int(time.time() * 1000) } response = self.request("POST", "read/end", headers, data) if response["code"] == "200": # self.log(f"结束阅读: {response['msg']}", level="info") return response else: self.log(f"结束阅读: {response['msg']}", level="error") return False def read_end_chapter(self, headers,readChapterId): """ 结束阅读章节 :param readChapterId: 阅读章节ID """ data = { "isBuy": "0", "isFee": "0", "isUserMember": "0", "isBookMember": "0", "readChapterId": readChapterId, "timestamp": int(time.time() * 1000) } response = self.request("POST", "read/endChapter", headers, data) if response["code"] == "200": # self.log(f"结束阅读章节: {response['msg']}", level="info") return response else: self.log(f"结束阅读章节: {response['msg']}", level="error") return False def receive_task_reward(self, headers, task_id, reward_id): """ 领取任务奖励 :param task_id: 任务ID :param reward_id: 奖励ID """ data = { "taskId": task_id, "rewardId": reward_id } response = self.request("POST", "task/receiveTaskReward", headers, data) if response["code"] == "200": self.log(f"领取奖励: {response['msg']}", push=False) return response else: self.log(f"领取奖励: {response['msg']}", level="error") return False def get_user_info(self, headers): """ 获取用户信息 """ response = self.request("GET", "account/getAccountByUser", headers, {}) if response["code"] == "200": nikeName = response["data"]["accountInfo"]["nickName"] # 代币 couponBalance = float(response["data"]["accountInfo"]["couponBalance"]) # 推荐票 dayCount = int(response["data"]["accountInfo"]["dayCount"]) self.log(f"{nikeName} 代币:{couponBalance} 推荐票:{dayCount}") return couponBalance else: self.log(f"获取用户信息: {response['msg']}", level="error") return False def get_book_chapter_info(self, headers, bookId): """ 获取小说章节信息 :param bookId: 小说ID :return: 小说名称, 章节列表 """ data = { "sortType": "1", "pageNo": "1", "pageSize": "9999", "bookId": bookId, "timestamp": int(time.time() * 1000) } response = self.request("GET", "chapter/getChapterListByBookId", headers, data) if response["code"] == "200": book_name = response["data"]["bookChapter"]["bookName"] chapter_list = response["data"]["bookChapter"]["chapterList"] return book_name, chapter_list else: self.log(f"获取小说章节信息: {response['msg']}", level="error") return False def order_book_chapter(self, headers, bookId, chapterId): """ 订阅章节 :param bookId: 小说ID :param chapterId: 章节ID """ data = { "buyCount": "1", "productId": chapterId, "viewType": "2", "consumeType": "1", "bookId": bookId, "timestamp": int(time.time() * 1000) } response = self.request("POST", "order/consume", headers, data) if response["code"] == "200": return True else: self.log(f"订阅章节: {response['msg']}", level="error") return False def run_for_account(self, index, token, deviceno): self.local.account_prefix = f"[账号{index}] " # 防止串号 headers = { "channel": "25", "deviceno": deviceno, "platform": "1", "imei": "", "targetmodel": "M2012K11AC", "oaid": "", "version": "3.4.3", "token": token, "user-agent": "Mozilla/5.0 (Linux; Android 11; Pixel 4 XL Build/RP1A.200720.009; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/92.0.4515.115 Mobile Safari/537.36" } # 签到 self.signin(headers) # 获取任务列表 task_list = self.get_task_list(headers) reader_time = 0 for task in task_list: if task['actionName'] == "阅读" and task['taskState'] != "0": reader_time = int(task['rewardList'][0]['actionRequire']) if reader_time >= 0: need_read_time = 50 - reader_time self.log(f"还需要阅读{need_read_time}分钟") if need_read_time > 0: # 5分钟一次,太长时间会失效 for i in range(need_read_time // 5): # 开始阅读 readId = self.read_start(headers) time.sleep(5) if readId: # 阅读章节 readChapterId = self.read_start_chapter(headers, readId) self.log(f"模拟第{i+1}次阅读中,请勿停止运行脚本", push=False) time.sleep(random.randint(300, 320)) if readChapterId: # 结束阅读章节 self.read_end_chapter(headers, readChapterId) time.sleep(5) # 结束阅读 self.read_end(headers, readId) else: self.log(f"已阅读{reader_time}分钟,跳过模拟阅读") # 获取任务列表 task_list = self.get_task_list(headers) if task_list: # 领取奖励 for task in task_list: if task["taskState"] == "1": self.receive_task_reward(headers, task["taskId"], task["rewardId"]) # 再次获取任务列表 task_list = self.get_task_list(headers) if task_list: for task in task_list: taskname = task["taskName"] if "阅读" in taskname: status = "已完成" if task["taskState"] == "2" else "未完成" self.log(f"任务-{taskname}-{status}", level="info") # 获取用户信息 coupon_balance = self.get_user_info(headers) # 自动订阅小说 if AUTO_ORDER_BOOK_ID and index == 1: book_name, chapter_list = self.get_book_chapter_info(headers, AUTO_ORDER_BOOK_ID) if chapter_list: for index, chapter in enumerate(chapter_list, 1): # 大于等于指定章节开始订阅 if index >= AUTO_ORDER_BOOK_CHAPTER and int(chapter['isFee']) == 1 and int(chapter['isBuy']) == 0: if coupon_balance >= float(chapter['price']): # 订阅 if self.order_book_chapter(headers, AUTO_ORDER_BOOK_ID, chapter["chapterId"]): self.log(f"订阅 [{book_name}] - {chapter['chapterName']} 成功") coupon_balance -= float(chapter['price']) else: break def run(self): self.log(f"【{self.script_name}】开始执行任务") threads = [] for index, (deviceno, token) in enumerate(self.check_env(), 1): t = threading.Thread( target=self.run_for_account, args=(index, token, deviceno) ) t.start() threads.append(t) for t in threads: t.join() if NOTIFY: # 如果notify模块不存在,从远程下载至本地 if not os.path.exists("notify.py"): url = "https://raw.githubusercontent.com/whyour/qinglong/refs/heads/develop/sample/notify.py" response = requests.get(url) with open("notify.py", "w", encoding="utf-8") as f: f.write(response.text) import notify else: import notify # 任务结束后推送日志 title = f"{self.script_name} 运行日志" header = "作者:临渊\n\n" content = header + "\n" +"\n".join(self.log_msgs) notify.send(title, content) if __name__ == "__main__": auto_task = AutoTask("次元姬小说") auto_task.run()