ksjsb/main.py
2026-07-30 21:38:10 +08:00

1623 lines
86 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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.

from __future__ import annotations
import argparse
import base64
import gzip
import hashlib
import json
import os
import re
import time
import urllib.error
import urllib.parse
import urllib.request
import zlib
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from core.device_cookie import apply_device_profile_to_cookie, cookie_to_string
from core.device_profile import DeviceProfile, DeviceProfileGenerator, load_device_profile
from core.enc_data import (
KWSG_10400_DEFAULT_CFG9,
build_inner_zt_header,
kwsg_10400_ecb_encrypt,
kwsg_10400_nonce9,
load_kwsg_10400_tables,
zt_outer_wrap,
)
from core.h5_sig3 import h5_sig3_crc32_from_sign_input, h5_sig3_from_fields
from core.h5_jsbridge import H5JsBridgeEncoder, build_h5_sign_input, h5_sig3_for_request
from core.h5_kww import H5KwwGenerator, PureH5KwwGenerator
from core.h5_kws import KWS_PRODUCT_NAME, build_h5_kws_default_ticket, build_h5_kws_script_ticket
from core.http_transport import create_http_session
from core.reward_request import build_reward_body, build_reward_str_e
from core.sig import build_sig_plaintext, sig as calc_sig
from core.sig3 import Kwsg10418State, load_kwsg_10418_tables
from core.tokensig import tokensig as calc_tokensig
from core.weapon_kas import WeaponProofProvider
from core.xfalcon import xfalcon_value_from_input_bytes
from tools.new_device import run_online_bootstrap
DEFAULT_OUT_DIR = Path("out")
NEBULA_HOST = "nebula.kuaishou.com"
API_HOST = "api.e.kuaishou.com"
NEBULA_BASE = "https://nebula.kuaishou.com"
API_BASE = "https://api.e.kuaishou.com"
SOURCE = "bottom_guide_first"
REPORT_ENC_HEAD8 = bytes.fromhex("5a54eecdeb3d358c")
REPORT_ENC_XOR_KEY = b"ErC76yvDSrCzakT3"
SENSITIVE_COOKIE_KEYS = {"kuaishou.api_st", "token", "kuaishou.h5_st", "kwssectoken", "kwscode", "kwfv1"}
SENSITIVE_QUERY_KEYS = {"__NStokensig", "__NS_sig3", "__NS_xfalcon", "kuaishou.api_st", "token"}
SIGNATURE_QUERY_KEYS = {"sig", "sig2", "__NS_sig3", "__NStokensig", "__NS_xfalcon"}
# 这些是从 2026-07-10 HAR 固化出的稳定协议字段,不再作为请求列表驱动主流程。
# 主流程见 KsNebulaClient.run_all_once(),每个接口均有独立方法。
H5_HEADERS = {'login': {'Host': 'nebula.kuaishou.com', 'sec-ch-ua-platform': '"Android"', 'User-Agent': 'Mozilla/5.0 (Linux; Android 16; PJZ110 Build/BP2A.250605.015; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.7827.164 Mobile Safari/537.36 Yoda/3.2.20 ksNebula/14.5.50.11631 OS_PRO_BIT/64 MAX_PHY_MEM/15107 KDT/PHONE AZPREFIX/az3 ICFO/0 StatusHT/40 TitleHT/252 NetType/NR ISLP/0 ISDM/0 ISLB/0 locale/zh-cn SHP/2376 SWP/1080 SD/3.0 CT/0 ISLM/0', 'sec-ch-ua': '"Android WebView";v="149", "Chromium";v="149", "Not)A;Brand";v="24"', 'sec-ch-ua-mobile': '?1', 'Accept': '*/*', 'X-Requested-With': 'com.kuaishou.nebula', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Dest': 'empty', 'Referer': 'https://nebula.kuaishou.com/nebula/task/earning?&layoutType=4&source=bottom_guide_first', 'Accept-Encoding': 'gzip', 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7'}, 'task_list': {'Host': 'nebula.kuaishou.com', 'Pragma': 'no-cache', 'Cache-Control': 'no-cache', 'sec-ch-ua-platform': '"Android"', 'User-Agent': 'Mozilla/5.0 (Linux; Android 16; PJZ110 Build/BP2A.250605.015; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.7827.164 Mobile Safari/537.36 Yoda/3.2.20 ksNebula/14.5.50.11631 OS_PRO_BIT/64 MAX_PHY_MEM/15107 KDT/PHONE AZPREFIX/az3 ICFO/0 StatusHT/40 TitleHT/252 NetType/NR ISLP/0 ISDM/0 ISLB/0 locale/zh-cn SHP/2376 SWP/1080 SD/3.0 CT/0 ISLM/0', 'sec-ch-ua': '"Android WebView";v="149", "Chromium";v="149", "Not)A;Brand";v="24"', 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', 'kww': 'KQKRjWEINlzEI07iMa8YrvXGWc4qQ2YMsjv4wGL2oLDzOX3k0vIz1mXqAtSlzbvkjuY8emvk2CTvxX4OajiTLTXaV5saHH2zG1eNE7Lh7q/izIB39vwqovlepXQ9wwG11DVzphkMKOLcXjHnGkOkvxg7k0mZIGMUoSxgoWaQeXtYEO+WRYQlSt92TO+KI7PKTQe4he/yhs/GHqBqUk0czIsgF==', 'sec-ch-ua-mobile': '?1', 'Accept': '*/*', 'X-Requested-With': 'com.kuaishou.nebula', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Dest': 'empty', 'Referer': 'https://nebula.kuaishou.com/nebula/task/earning?&layoutType=4&source=bottom_guide_first', 'Accept-Encoding': 'gzip', 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7'}, 'sign_in_resource': {'Host': 'nebula.kuaishou.com', 'Pragma': 'no-cache', 'Cache-Control': 'no-cache', 'sec-ch-ua-platform': '"Android"', 'User-Agent': 'Mozilla/5.0 (Linux; Android 16; PJZ110 Build/BP2A.250605.015; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.7827.164 Mobile Safari/537.36 Yoda/3.2.20 ksNebula/14.5.50.11631 OS_PRO_BIT/64 MAX_PHY_MEM/15107 KDT/PHONE AZPREFIX/az3 ICFO/0 StatusHT/40 TitleHT/252 NetType/NR ISLP/0 ISDM/0 ISLB/0 locale/zh-cn SHP/2376 SWP/1080 SD/3.0 CT/0 ISLM/0', 'sec-ch-ua': '"Android WebView";v="149", "Chromium";v="149", "Not)A;Brand";v="24"', 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', 'kww': 'KQKRjWEINlzEI07iMa8YrvXGWc4qQ2YMsjv4wGL2oLDzOX3k0vIz1mXqAtSlzbvkjuY8emvk2CTvxX4OajiTLTXaV5saHH2zG1eNE7Lh7q/izIB39vwqovlepXQ9wwG11DVzphkMKOLcXjHnGkOkvxg7k0mZIGMUoSxgoWaQeXtYEO+WRYQlSt92TO+KI7PKTQe4he/yhs/GHqBqUk0czIsgF==', 'sec-ch-ua-mobile': '?1', 'Accept': '*/*', 'X-Requested-With': 'com.kuaishou.nebula', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Dest': 'empty', 'Referer': 'https://nebula.kuaishou.com/nebula/task/earning?&layoutType=4&source=bottom_guide_first', 'Accept-Encoding': 'gzip', 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7'}, 'sign_in_report': {'Host': 'nebula.kuaishou.com', 'Pragma': 'no-cache', 'Cache-Control': 'no-cache', 'sec-ch-ua-platform': '"Android"', 'User-Agent': 'Mozilla/5.0 (Linux; Android 16; PJZ110 Build/BP2A.250605.015; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.7827.164 Mobile Safari/537.36 Yoda/3.2.20 ksNebula/14.5.50.11631 OS_PRO_BIT/64 MAX_PHY_MEM/15107 KDT/PHONE AZPREFIX/az3 ICFO/0 StatusHT/40 TitleHT/252 NetType/NR ISLP/0 ISDM/0 ISLB/0 locale/zh-cn SHP/2376 SWP/1080 SD/3.0 CT/0 ISLM/0', 'sec-ch-ua': '"Android WebView";v="149", "Chromium";v="149", "Not)A;Brand";v="24"', 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', 'kww': 'PnGU+9+Y8008S+nH0U+0mjPf8fP08f+98f+nLlwnrIP9+0G0G7+/z08/8jP/ZEw/80Gf+D8/PUPeHFPfGhPfLE+fbYGnHEwBLEGnr7+BP7Per9w/Sf+AD7GfLIG/rE+ncl8Br7Gnb0+B8S+/rIP98D+nzD8BLh80rIP/pSwnpYPBL=', 'sec-ch-ua-mobile': '?1', 'Accept': '*/*', 'X-Requested-With': 'com.kuaishou.nebula', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Dest': 'empty', 'Referer': 'https://nebula.kuaishou.com/nebula/task/earning?&layoutType=4&source=bottom_guide_first', 'Accept-Encoding': 'gzip', 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7'}, 'treasure_info': {'Host': 'nebula.kuaishou.com', 'Pragma': 'no-cache', 'Cache-Control': 'no-cache', 'sec-ch-ua-platform': '"Android"', 'User-Agent': 'Mozilla/5.0 (Linux; Android 16; PJZ110 Build/BP2A.250605.015; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.7827.164 Mobile Safari/537.36 Yoda/3.2.20 ksNebula/14.5.50.11631 OS_PRO_BIT/64 MAX_PHY_MEM/15107 KDT/PHONE AZPREFIX/az3 ICFO/0 StatusHT/40 TitleHT/252 NetType/NR ISLP/0 ISDM/0 ISLB/0 locale/zh-cn SHP/2376 SWP/1080 SD/3.0 CT/0 ISLM/0', 'sec-ch-ua': '"Android WebView";v="149", "Chromium";v="149", "Not)A;Brand";v="24"', 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', 'kww': 'KQKRjWEINlzEI07iMa8YrvXGWc4qQ2YMsjv4wGL2oLDzOX3k0vIz1mXqAtSlzbvkjuY8emvk2CTvxX4OajiTLTXaV5saHH2zG1eNE7Lh7q/izIB39vwqovlepXQ9wwG11DVzphkMKOLcXjHnGkOkvxg7k0mZIGMUoSxgoWaQeXtYEO+WRYQlSt92TO+KI7PKTQe4he/yhs/GHqBqUk0czIsgF==', 'sec-ch-ua-mobile': '?1', 'Accept': '*/*', 'X-Requested-With': 'com.kuaishou.nebula', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Dest': 'empty', 'Referer': 'https://nebula.kuaishou.com/nebula/task/earning?&layoutType=4&source=bottom_guide_first', 'Accept-Encoding': 'gzip', 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7'}, 'treasure_open': {'Host': 'nebula.kuaishou.com', 'Pragma': 'no-cache', 'Cache-Control': 'no-cache', 'sec-ch-ua-platform': '"Android"', 'User-Agent': 'Mozilla/5.0 (Linux; Android 16; PJZ110 Build/BP2A.250605.015; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.7827.164 Mobile Safari/537.36 Yoda/3.2.20 ksNebula/14.5.50.11631 OS_PRO_BIT/64 MAX_PHY_MEM/15107 KDT/PHONE AZPREFIX/az3 ICFO/0 StatusHT/40 TitleHT/252 NetType/NR ISLP/0 ISDM/0 ISLB/0 locale/zh-cn SHP/2376 SWP/1080 SD/3.0 CT/0 ISLM/0', 'sec-ch-ua': '"Android WebView";v="149", "Chromium";v="149", "Not)A;Brand";v="24"', 'content-type': 'application/json', 'kww': 'PnGU+9+Y8008S+nH0U+0mjPf8fP08f+98f+nLlwnrIP9+0G0G7+/z08/8jP/ZEw/80Gf+D8/PUPeHFPfGhPfLE+fbYGnHEwBLEGnr7+BP7Per9w/Sf+AD7GfLIG/rE+ncl8Br7Gnb0+B8S+/rIP98D+nzD8BLh80rIP/pSwnpYPBL=', 'sec-ch-ua-mobile': '?1', 'Accept': '*/*', 'Origin': 'https://nebula.kuaishou.com', 'X-Requested-With': 'com.kuaishou.nebula', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Dest': 'empty', 'Referer': 'https://nebula.kuaishou.com/nebula/task/earning?&layoutType=4&source=bottom_guide_first', 'Accept-Encoding': 'gzip', 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7'}}
TASK_LIST_PARAMS = {'searchWidgetStatus': 'false', 'rankWidgetStatus': 'false', 'encourageDesktopWidgetStatus': 'false', 'pushNotificationStatus': 'true', 'encourageDesktopWidgetNewStatus': '0', 'cs': 'false', 'source': 'bottom_guide_first', 'bizParams': '{"ignoringBatteryOptimizationsStatus":false,"source":"bottom_guide_first","initRequest":true,"refreshType":"init","contactsPermissionStatus":false}', 'imei': '', 'idfa': '', 'oaid': '700ED68650EF49ECB686B2AAA9994C1Dca31c779ee5acb7d4c3b03337599a591'}
SIGN_IN_EVENT_TRACKING = '{"deliverOrderId":"5118","eventTrackingTaskId":20022,"resourceId":"externalFeed_externalSignPopup","extParams":{"signInThresholdTaskType":0,"autoSignInSuccess":false,"source":"bottom_guide_first"},"resourceFirstLevelName":"EXTERNAL_FEED","resourceSecondLevelName":"EXTERNAL_SIGN_POPUP"}'
TREASURE_OPEN_BODY = '{"imei":"","idfa":"","oaid":"700ED68650EF49ECB686B2AAA9994C1Dca31c779ee5acb7d4c3b03337599a591","eventTrackingLogInfo":"{\\"eventTrackingTaskId\\":20035,\\"resourceId\\":\\"externalFeed_externalTreasureChestWidget\\",\\"extParams\\":{\\"isServerRecordClickAction\\":true,\\"source\\":\\"bottom_guide_first\\"},\\"deliverOrderId\\":\\"5128\\",\\"resourceFirstLevelName\\":\\"EXTERNAL_FEED\\",\\"resourceSecondLevelName\\":\\"EXTERNAL_TREASURE_CHEST_WIDGET\\"}"}'
DEFAULT_OAID = TASK_LIST_PARAMS["oaid"]
TREASURE_OPEN_EVENT_TRACKING = {
"eventTrackingTaskId": 20035,
"resourceId": "externalFeed_externalTreasureChestWidget",
"extParams": {"isServerRecordClickAction": True, "source": SOURCE},
"deliverOrderId": "5128",
"resourceFirstLevelName": "EXTERNAL_FEED",
"resourceSecondLevelName": "EXTERNAL_TREASURE_CHEST_WIDGET",
}
H5_SIG3_CRC32 = {
"sign_in_resource": 0x2C60DFFD,
"sign_in_report": 0xCCAE724F,
"treasure_info": 0xA6E9C286,
"treasure_open": 0x02F88937,
}
API_HEADERS = {'Host': 'api.e.kuaishou.com', 'User-Agent': 'kwai-android aegon/4.50.1', 'Accept-Language': 'zh-cn', 'page-code': 'NEW_TASK_CENTER', 'kaw': 'MDJ3ll0yLjVpYO7iiytguFDgh1dM9kzX1QW7XXr0RScGn+UbPnUe/UT8OM+5ods5QMOzXRclcl/CzL9jxwWWfBFEYYSzM7RbUPpceM0luB7tOTcbSVlRfDDvYPWVAYRZ5ETRRvOM5sUIG7mB25kTIy2Q90Q/XaCQdkDCy0ikkyN0zL30NUoEBAt5t6tg8lXHTgMhuGIHxxmLccoTvk6dC0ZJhKgGdMmK3lMwGnbxyb3vjKl5tSXFx7nC2Z0ES1PnoYBhwpsA', 'kas': '0013d223e281e58ae60aea1b9948d88f72', 'Content-Type': 'application/x-www-form-urlencoded', 'Accept-Encoding': 'gzip', 'X-Client-Info': 'model=PJZ110;os=Android;nqe-score=-1;network=OTHER;'}
API_BASE_PARAMS = {'earphoneMode': '1', 'mod': 'OnePlus(PJZ110)', 'appver': '14.5.50.11631', 'isp': 'CUCC', 'language': 'zh-cn', 'ud': '1579452490', 'did_tag': '0', 'egid': 'DFPE1EAE3D15BCBB5132EF6598C43DC060E5166B02182E3800A82577C54EE4E1', 'thermal': '10000', 'net': '5G', 'kcv': '1627', 'app': '0', 'kpf': 'ANDROID_PHONE', 'bottom_navigation': 'true', 'ver': '14.5', 'android_os': '0', 'oDid': 'ANDROID_46a032e0a2af8184', 'boardPlatform': 'sun', 'kpn': 'NEBULA', 'newOc': 'OPPO', 'androidApiLevel': '36', 'slh': '0', 'country_code': 'cn', 'nbh': '0', 'hotfix_ver': '', 'did_gt': '1783674614984', 'keyconfig_state': '2', 'cdid_tag': '2', 'sys': 'ANDROID_16', 'max_memory': '256', 'cold_launch_time_ms': '1783674610031', 'oc': 'OPPO', 'sh': '2376', 'deviceBit': '4', 'browseType': '3', 'ddpi': '480', 'socName': 'Qualcomm Snapdragon 8750', 'is_background': '0', 'c': 'OPPO', 'sw': '1080', 'ftt': '', 'apptype': '22', 'abi': 'arm64', 'userRecoBit': '0', 'device_abi': 'arm64', 'icaver': '1', 'totalMemory': '15107', 'grant_browse_type': 'AUTHORIZED', 'iuid': '', 'rdid': 'ANDROID_741de4351c44850d', 'sbh': '120', 'darkMode': 'false', 'did': 'ANDROID_e8dfd2f16b618053'}
API_COMMON_BODY = {'cs': 'false', 'client_key': '2ac2a76d', 'videoModelCrowdTag': '1_100', 'os': 'android', 'uQaTag': '7#33333333339999999999#DP:3hX9ONf4GgQVINru4wfjCg==#ecBl:33#ecPp:-9#cmNt:-1#cmHs:-5#cmMnsl:-0#cmAu:-3'}
API_SIG3_SEED = {'sign_in_fetch': 'a6b7c2e4c8969fbd4feeedecfe49ab42d341a59ef3fff1e7', 'treasure_fetch': 'e8f98caa86d8d1f31ca0a3a2ed9f44b9dd0febd0bdb1bfa9', 'normal_fetch': 'dacbbe98b4eae3c18093919051c3bf2a163dd9e28f838d9b', 'sign_in_report': 'c1d0a583aff1f8da32898a8b3119641ed926c2f994989680', 'treasure_report': '3c2d587e520c0527967477767f4f25c8ffdb3f0469656b7d', 'normal_report': 'aebfcaecc09e97b5cbe7e5e4bbaf01124349ad96fbf7f9ef'}
AD_FETCH_PAYLOADS = {'sign_in': {'encData': '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', 'sign': '5a54eecde4d4ea61056e396d684a1637d6584231c3c5598e70987b175323786d'}, 'treasure': {'encData': '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', 'sign': '5a54eecde4d4ea61e78cdb8f8aa8f4d517baa0d314abe099d27a99f5b1c19a8f'}, 'normal': {'encData': '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', 'sign': '5a54eecde4d4ea61c6adfaaeab89d5f4989a81f29c36b66b0a5bb8d490e0bbae'}}
AD_FETCH_FALLBACK_BUSINESS = {"sign_in": 605, "treasure": 606, "normal": 672}
AD_FETCH_FALLBACK_SCENE = {"sign_in": (11101, 100024058, 20342), "treasure": (11101, 100024064, 20346), "normal": (11101, 100026367, 24067)}
def describe_local_algorithm_stack(
*,
h5_sig3_mode: str | None = None,
h5_kww_mode: str | None = None,
h5_kws_mode: str | None = None,
report_body_mode: str | None = None,
) -> list[str]:
"""返回当前任务使用的本地算法栈说明。
这里只列本地 Python 模块和本地 Node runner避免把参考脚本里的
远端签名服务混入执行路径。
"""
sig3_mode = (h5_sig3_mode or os.environ.get("KS_H5_SIG3_MODE", "pure")).strip().lower() or "pure"
kww_mode = (h5_kww_mode or os.environ.get("KS_H5_KWW_MODE", "pure")).strip().lower() or "pure"
kws_mode = (h5_kws_mode or os.environ.get("KS_H5_KWS_MODE", "default")).strip().lower() or "default"
body_mode = (report_body_mode or os.environ.get("KS_REPORT_BODY_MODE", "encData")).strip() or "encData"
h5_sig3_impl = "core.h5_sig3"
if sig3_mode in {"jsbridge", "node", "vm", "jsbridge_strict", "strict"}:
h5_sig3_impl = "core.h5_jsbridge + local node runner"
h5_kww_impl = "core.h5_kww"
if kww_mode not in {"pure", "python", "py", "0", "off", "static", "disabled"}:
h5_kww_impl = "core.h5_kww + local node runner"
h5_kws_impl = "core.h5_kws default ticket"
if kws_mode in {"script", "sign", "server", "config"}:
h5_kws_impl = "core.h5_kws local signUrl runner / known-code parser"
return [
"api_query: sig=core.sig; api_sig3=core.sig3; xfalcon=core.xfalcon; tokensig=core.tokensig",
"reward_fetch: reward_fetch=core.reward_request; encData=core.enc_data; body_sign=core.reward_sign",
f"reward_report: body_mode={body_mode}; report_enc=core.enc_data; url_sign=core.sig/core.sig3/core.xfalcon",
f"h5_state: h5_sig3={h5_sig3_impl}; sign_input=core.h5_jsbridge.build_h5_sign_input",
f"h5_webweapon: kws={h5_kws_impl}; kww={h5_kww_impl}",
]
def print_local_algorithm_stack() -> None:
print("[算法] 本地纯算链路启用")
for line in describe_local_algorithm_stack():
print(f"[算法] {line}")
_SIG3_TABLES: tuple[bytes, bytes] | None = None
_ENC_TABLES: tuple[bytes, bytes] | None = None
def _compact_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
def event_tracking_to_json(value: Any) -> str:
if isinstance(value, str):
text = value.strip()
if not text:
return ""
try:
value = json.loads(text)
except json.JSONDecodeError:
return text
if isinstance(value, dict):
return _compact_json(value)
return ""
def event_tracking_with_source(value: Any) -> str:
"""把 H5 状态上报事件归一为 HAR 成功样本使用的 source 上下文。"""
if isinstance(value, str):
text = value.strip()
if not text:
return ""
try:
value = json.loads(text)
except json.JSONDecodeError:
return text
if not isinstance(value, dict):
return ""
event = json.loads(_compact_json(value))
ext_params = event.get("extParams")
if not isinstance(ext_params, dict):
ext_params = {}
ext_params.setdefault("source", SOURCE)
event["extParams"] = ext_params
return _compact_json(event)
_H5_COOKIE_WIRE_ENCODE_KEYS = {"mod", "deviceName", "socName"}
def normalize_h5_cookie_wire_values(cookie: dict[str, str]) -> dict[str, str]:
"""把 `.env` 中易读设备字段恢复成 APP Cookie 的 wire 形态。
浏览器/HAR 中 `mod`、`socName` 等字段保留 URL 编码形态:
`OnePlus%28PJZ110%29`、`Qualcomm+Snapdragon+8750`。
`.env` 为了可读通常写成 `OnePlus(PJZ110)` 和空格形式H5
`__NS_sig3` 的 sign_input 又直接读取 cookie 值,因此这里在 H5
专用 cookie 中归一API query 仍走原始字段后由 urlencode 编码。
"""
out = dict(cookie)
for key in _H5_COOKIE_WIRE_ENCODE_KEYS:
value = out.get(key)
if value is None:
continue
decoded = urllib.parse.unquote_plus(str(value))
out[key] = urllib.parse.quote_plus(decoded, safe="")
return out
def treasure_open_body_for_oaid(oaid: str, event_tracking: Any = None) -> str:
event_tracking_json = event_tracking_to_json(event_tracking)
if not event_tracking_json:
event_tracking_json = _compact_json(TREASURE_OPEN_EVENT_TRACKING)
return _compact_json(
{
"imei": "",
"idfa": "",
"oaid": oaid,
"eventTrackingLogInfo": event_tracking_json,
}
)
@dataclass(frozen=True)
class AdMaterial:
kind: str
llsid: str
creative_id: str
business_id: int
page_id: int
sub_page_id: int
pos_id: int
material_time: int
request_scene_type: int
neo_params: str = ""
neo_ext_params: str = ""
ad_ext_info: str = ""
callback_param: str = ""
feed_id: str = ""
session_id: str = ""
@dataclass
class ResponseRecord:
label: str
title: str
request_id: int
status: int | None
ok: bool
url: str
response_json: Any
response_text: str
error: str = ""
def summary(self) -> str:
if isinstance(self.response_json, dict) and self.response_json.get("dryRun") is True:
return (
f"dry-run method={self.response_json.get('method')} "
f"host={self.response_json.get('host')} "
f"path={self.response_json.get('path')} "
f"bodySize={self.response_json.get('bodySize')}"
)
if isinstance(self.response_json, dict) and self.response_json.get("skipped") is True:
parts = ["skipped"]
reason = self.response_json.get("reason")
expected_reward = self.response_json.get("expectedReward")
countdown_ms = self.response_json.get("countdownMs")
if reason:
parts.append(f"reason={reason}")
if expected_reward:
parts.append(f"expected={expected_reward}金币")
if countdown_ms:
parts.append(f"countdown={float(countdown_ms) / 1000:.1f}s")
return " ".join(parts)
if self.error:
return f"HTTP={self.status or '-'} error={self.error}"
result = self.response_json.get("result") if isinstance(self.response_json, dict) else None
msg = extract_message(self.response_json)
awards = extract_awards(self.response_json)
parts = [f"HTTP={self.status}", f"result={result}"]
if msg:
parts.append(f"msg={msg}")
expected_reward = extract_fetch_expected_reward(self.response_json)
if expected_reward:
parts.append(f"expected={expected_reward}金币")
material_time = extract_fetch_material_time(self.response_json)
if material_time:
parts.append(f"material={material_time / 1000:.1f}s")
if awards:
parts.append("award=" + ", ".join(awards[:4]))
return " ".join(parts)
def to_json(self) -> dict[str, Any]:
return {
"label": self.label,
"title": self.title,
"requestId": self.request_id,
"status": self.status,
"ok": self.ok,
"url": redact_url(self.url),
"error": self.error,
"summary": self.summary(),
"responseJson": redact_json(self.response_json),
"responseText": "" if self.response_json is not None else self.response_text[:2000],
}
@dataclass(frozen=True)
class DeviceRuntimeOptions:
memory_device: bool
device_online: bool
rotate_h5_device: bool = False
strict_device_online: bool = False
def load_dotenv(path: Path = Path(".env")) -> None:
if not path.exists():
return
for raw in path.read_text(encoding="utf-8", errors="replace").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
value = value[1:-1]
os.environ.setdefault(key.strip(), value)
def parse_account(value: str) -> tuple[str, str]:
parts = value.split("#")
if len(parts) >= 3:
return parts[1].strip(), parts[2].strip()
return "", ""
def parse_first_ksck(value: str) -> tuple[str, str]:
"""解析单变量 `ksck`,多个账号按 `\\n` / `\\r` 分隔,默认取第一个。"""
text = (value or "").replace("\\r", "\r").replace("\\n", "\n")
for raw in re.split(r"[\r\n]+", text):
item = raw.strip()
if not item:
continue
cookie, salt = parse_account(item)
if cookie:
return cookie, salt
return item, ""
return "", ""
def parse_cookie(cookie: str) -> dict[str, str]:
out: dict[str, str] = {}
for item in cookie.split(";"):
item = item.strip()
if "=" in item:
key, value = item.split("=", 1)
out[key.strip()] = value.strip()
return out
def cookie_from_env(device_profile_path: str | Path | None = None) -> tuple[str, str, dict[str, str]]:
ksck_cookie, ksck_salt = parse_first_ksck(os.environ.get("ksck") or os.environ.get("KSCK", ""))
account_cookie, account_salt = parse_account(os.environ.get("KS_ACCOUNT", ""))
if ksck_cookie:
cookie = ksck_cookie
client_salt = ksck_salt or os.environ.get("KS_CLIENT_SALT") or account_salt
else:
cookie = os.environ.get("KS_COOKIE") or account_cookie
client_salt = os.environ.get("KS_CLIENT_SALT") or account_salt
if not cookie:
raise SystemExit("缺少 ksck / KS_COOKIE / KS_ACCOUNT请先配置 .env")
cookie_dict = parse_cookie(cookie)
profile_path = str(device_profile_path or os.environ.get("KS_DEVICE_PROFILE", "")).strip()
if profile_path:
profile = load_device_profile(profile_path)
cookie_dict = apply_device_profile_to_cookie(cookie_dict, profile)
cookie = cookie_to_string(cookie_dict)
return cookie, client_salt, cookie_dict
def args_to_device_options(args: argparse.Namespace) -> DeviceRuntimeOptions:
server_device = bool(getattr(args, "server_device", False))
no_device_online = bool(getattr(args, "no_device_online", False))
if server_device and no_device_online:
raise SystemExit("--server-device conflicts with --no-device-online")
return DeviceRuntimeOptions(
memory_device=bool(getattr(args, "memory_device", False)) or server_device,
device_online=not no_device_online,
rotate_h5_device=bool(getattr(args, "rotate_h5_device", False)),
strict_device_online=bool(getattr(args, "strict_device_online", False)),
)
def safe_int(value: Any, default: int = 0) -> int:
try:
if value in (None, ""):
return default
text = str(value).strip()
try:
return int(text)
except ValueError:
return int(float(text))
except Exception:
return default
def generate_request_id() -> str:
return f"{int(time.time() * 1000)}{os.getpid() % 100000:05d}"
def sig3_tables() -> tuple[bytes, bytes]:
global _SIG3_TABLES
if _SIG3_TABLES is None:
_SIG3_TABLES = load_kwsg_10418_tables("bin/kwsg_10418_B_T1.bin", "bin/kwsg_10418_B_T2.bin")
return _SIG3_TABLES
def enc_tables() -> tuple[bytes, bytes]:
global _ENC_TABLES
if _ENC_TABLES is None:
_ENC_TABLES = load_kwsg_10400_tables("bin/kwsg_10400_T1.bin", "bin/kwsg_10400_T2.bin")
return _ENC_TABLES
def build_report_enc_data(payload: bytes) -> str:
t1, t2 = enc_tables()
encrypted = kwsg_10400_ecb_encrypt(payload, t1, t2)
inner = build_inner_zt_header(kwsg_10400_nonce9(), KWSG_10400_DEFAULT_CFG9, encrypted) + encrypted
raw = zt_outer_wrap(inner, REPORT_ENC_HEAD8, REPORT_ENC_XOR_KEY)
return base64.b64encode(raw).decode("ascii")
def replace_pair_values(pairs: list[tuple[str, str]], updates: dict[str, str]) -> list[tuple[str, str]]:
seen: set[str] = set()
result: list[tuple[str, str]] = []
for key, value in pairs:
if key in updates:
result.append((key, updates[key]))
seen.add(key)
else:
result.append((key, value))
for key, value in updates.items():
if key not in seen:
result.append((key, value))
return result
def signed_api_url(
path: str,
params: dict[str, str],
body_pairs: list[tuple[str, str]],
client_salt: str,
sig3_seed: str,
*,
state: Kwsg10418State | None = None,
) -> str:
signing_params = {k: v for k, v in params.items() if k not in SIGNATURE_QUERY_KEYS and not k.startswith("__NS")}
signing_params.update(dict(body_pairs))
sig_value = calc_sig(signing_params)
if state is None:
try:
state = Kwsg10418State.from_digest(sig3_seed)
except Exception:
state = Kwsg10418State()
t1, t2 = sig3_tables()
sig3_value = state.sig3_hex(path + sig_value, t1=t1, t2=t2)
token_sig = calc_tokensig(sig_value, client_salt) if client_salt else ""
xfalcon = xfalcon_value_from_input_bytes(path.encode("utf-8") + build_sig_plaintext(signing_params))
signed = dict(params)
signed.update({"sig": sig_value, "__NS_sig3": sig3_value, "__NStokensig": token_sig, "__NS_xfalcon": xfalcon})
return f"{API_BASE}{path}?{urllib.parse.urlencode(signed)}"
def decode_response(raw: bytes, encoding: str) -> str:
encoding = encoding.lower()
if "gzip" in encoding:
raw = gzip.decompress(raw)
elif "deflate" in encoding:
raw = zlib.decompress(raw)
return raw.decode("utf-8", "replace")
def parse_json(text: str) -> Any:
try:
return json.loads(text)
except Exception:
return None
def is_business_ok(status: int | None, data: Any) -> bool:
if status is None or not (200 <= status < 300):
return False
if isinstance(data, dict):
result = data.get("result")
message = extract_message(data)
if result in (50, 500, 6001) or data.get("code") == 400010:
return False
if message in {"ANTISPAM_REQUEST", "INVALID_REQUEST"}:
return False
return True
def is_rate_limited(status: int | None, data: Any) -> bool:
if status == 429:
return True
if not isinstance(data, dict):
return False
message = extract_message(data)
return data.get("code") == 400010 or "访问太快" in message
def extract_message(data: Any) -> str:
if not isinstance(data, dict):
return ""
for key in ("message", "errorMsg", "error_msg", "msg"):
if data.get(key):
return str(data[key])
node = data.get("data")
if isinstance(node, dict):
for key in ("toast", "errorMessage", "antispamToast"):
if node.get(key):
return str(node[key])
return ""
def extract_fetch_material_time(data: Any) -> int:
if not isinstance(data, dict):
return 0
feeds = data.get("feeds")
if not isinstance(feeds, list) or not feeds:
return 0
feed = feeds[0] if isinstance(feeds[0], dict) else {}
return safe_int((feed.get("ext_params") or {}).get("video"), 0)
def extract_fetch_expected_reward(data: Any) -> int:
if not isinstance(data, dict):
return 0
feeds = data.get("feeds")
if not isinstance(feeds, list) or not feeds:
return 0
feed = feeds[0] if isinstance(feeds[0], dict) else {}
ad = feed.get("ad") if isinstance(feed.get("ad"), dict) else {}
ad_v2 = ad.get("adDataV2") if isinstance(ad.get("adDataV2"), dict) else {}
for item in ad_v2.get("templateDatas") or []:
if not isinstance(item, dict) or not isinstance(item.get("data"), str):
continue
try:
template_data = json.loads(item["data"])
except Exception:
continue
neo_count = safe_int(template_data.get("neoCount"), 0)
if neo_count > 0:
return neo_count
return 0
def extract_awards(data: Any) -> list[str]:
awards: list[str] = []
def add(name: str, value: Any, unit: str = "") -> None:
if value in (None, "", []):
return
text = f"{name}:{value}{unit}"
if text not in awards:
awards.append(text)
def walk(node: Any) -> None:
if isinstance(node, dict):
if "neoAmount" in node:
add("neoAmount", node.get("neoAmount"), "金币")
if "sessionTotalAmount" in node:
add("sessionTotal", node.get("sessionTotalAmount"), "金币")
if "totalCoin" in node:
add("totalCoin", node.get("totalCoin"), "金币")
if "allCash" in node:
add("cash", node.get("allCash"), "")
if "rewardCount" in node:
add("reward", node.get("rewardCount"), str(node.get("rewardUnit") or ""))
if "amount" in node and any(k in node for k in ("awardType", "bizType")):
add(str(node.get("bizType") or "amount"), node.get("amount"), "金币")
for value in node.values():
if isinstance(value, (dict, list)):
walk(value)
elif isinstance(node, list):
for item in node:
walk(item)
walk(data)
return awards
def extract_neo_amount(data: Any) -> int | None:
if isinstance(data, dict):
if "neoAmount" in data:
return safe_int(data.get("neoAmount"), 0)
for value in data.values():
amount = extract_neo_amount(value)
if amount is not None:
return amount
elif isinstance(data, list):
for item in data:
amount = extract_neo_amount(item)
if amount is not None:
return amount
return None
def redact_url(url: str) -> str:
split = urllib.parse.urlsplit(url)
pairs = urllib.parse.parse_qsl(split.query, keep_blank_values=True)
pairs = [(k, "<redacted>" if k in SENSITIVE_QUERY_KEYS else v) for k, v in pairs]
return urllib.parse.urlunsplit((split.scheme, split.netloc, split.path, urllib.parse.urlencode(pairs), split.fragment))
def redact_json(value: Any) -> Any:
if isinstance(value, dict):
out = {}
for key, item in value.items():
key_s = str(key)
if key_s in SENSITIVE_COOKIE_KEYS or "token" in key_s.lower() or key_s.endswith("_st"):
out[key] = "<redacted>"
else:
out[key] = redact_json(item)
return out
if isinstance(value, list):
return [redact_json(item) for item in value]
return value
def decode_neo_params(value: str) -> dict[str, Any]:
if not value:
return {}
try:
padded = value + "=" * (-len(value) % 4)
return json.loads(base64.b64decode(padded).decode("utf-8"))
except Exception:
return {}
def extract_ad_material(kind: str, data: Any) -> AdMaterial | None:
if not isinstance(data, dict):
return None
feeds = data.get("feeds") or []
if not feeds:
return None
feed = feeds[0] if isinstance(feeds[0], dict) else {}
ad = feed.get("ad") if isinstance(feed.get("ad"), dict) else {}
ad_v2 = ad.get("adDataV2") if isinstance(ad.get("adDataV2"), dict) else {}
inspire = ad_v2.get("inspireAdInfo") if isinstance(ad_v2.get("inspireAdInfo"), dict) else {}
ext_data: dict[str, Any] = {}
raw_ext = ad.get("extData")
if isinstance(raw_ext, str) and raw_ext:
try:
ext_data = json.loads(raw_ext)
except Exception:
ext_data = {}
elif isinstance(raw_ext, dict):
ext_data = raw_ext
neo_params = str(inspire.get("neoParams") or "")
neo_decoded = decode_neo_params(neo_params)
business_id = safe_int(
neo_decoded.get("businessId") or ext_data.get("inspireTaskId") or AD_FETCH_FALLBACK_BUSINESS.get(kind),
AD_FETCH_FALLBACK_BUSINESS.get(kind, 0),
)
material_time = safe_int((feed.get("ext_params") or {}).get("video"), 0)
if material_time <= 0:
stream = feed.get("streamManifest") if isinstance(feed.get("streamManifest"), dict) else {}
sets = stream.get("adaptationSet") if isinstance(stream.get("adaptationSet"), list) else []
material_time = safe_int((sets[0] if sets else {}).get("duration"), 30000)
return AdMaterial(
kind=kind,
llsid=str(ad.get("llsid") or data.get("llsid") or ""),
creative_id=str(ad.get("creativeId") or ""),
business_id=business_id,
page_id=safe_int(neo_decoded.get("pageId") or ad.get("pageId"), 0),
sub_page_id=safe_int(neo_decoded.get("subPageId") or ad.get("subPageId"), 0),
pos_id=safe_int(ad_v2.get("posId"), 0),
material_time=material_time,
request_scene_type=safe_int(ext_data.get("requestSceneType"), 1),
neo_params=neo_params,
neo_ext_params=str(neo_decoded.get("extParams") or ""),
ad_ext_info=str(inspire.get("adExtInfo") or ""),
callback_param=str(ad.get("callbackParam") or ""),
feed_id=str(ext_data.get("liveStreamId") or feed.get("liveStreamId") or ""),
session_id=str(ext_data.get("inspireSessionId") or ""),
)
def fallback_ad_material(kind: str) -> AdMaterial:
page_id, sub_page_id, pos_id = AD_FETCH_FALLBACK_SCENE.get(kind, (11101, 0, 0))
return AdMaterial(
kind=kind,
llsid="0",
creative_id="0",
business_id=AD_FETCH_FALLBACK_BUSINESS.get(kind, 0),
page_id=page_id,
sub_page_id=sub_page_id,
pos_id=pos_id,
material_time=30000,
request_scene_type=1,
session_id=f"dryRun-{kind}-{int(time.time() * 1000)}",
)
def build_ad_report_biz(material: AdMaterial, cookie_dict: dict[str, str]) -> dict[str, Any]:
if material.neo_params:
neo_info = {
"llsid": safe_int(material.llsid),
"feedId": material.feed_id,
"creativeId": safe_int(material.creative_id),
"taskType": 1,
"extInfo": material.neo_params,
"requestSceneType": material.request_scene_type or 1,
"watchStage": 0,
"watchExpId": "",
"adExtInfo": material.ad_ext_info,
"watchAdTime": 0,
"materialTime": 0,
"inflow": False,
}
if material.callback_param:
neo_info["callbackParam"] = material.callback_param
return {
"pageId": material.page_id,
"subPageId": material.sub_page_id,
"posId": material.pos_id,
"businessId": material.business_id,
"mediaScene": "live",
"extParams": material.neo_ext_params,
"startTime": 0,
"endTime": 0,
"neoInfos": [neo_info],
"reportType": 0,
"sessionId": "",
}
end_time = int(time.time() * 1000)
watch_time = max(material.material_time, 5000)
start_time = end_time - watch_time
task_type = 2 if material.business_id == 606 else 1
request_scene_type = 7 if material.business_id == 606 else (material.request_scene_type or 1)
user_id = cookie_dict.get("userId") or cookie_dict.get("ud") or ""
session_id = material.session_id or (f"adNeo-{user_id}-{material.sub_page_id}-{end_time}" if material.business_id == 672 else "")
neo_info = {
"creativeId": safe_int(material.creative_id),
"extInfo": "",
"llsid": safe_int(material.llsid),
"materialTime": material.material_time,
"watchAdTime": watch_time,
"requestSceneType": request_scene_type,
"taskType": task_type,
"watchExpId": "",
"watchStage": 0,
}
if material.ad_ext_info:
neo_info["adExtInfo"] = material.ad_ext_info
if material.callback_param:
neo_info["callbackParam"] = material.callback_param
return {
"businessId": material.business_id,
"endTime": end_time,
"extParams": "",
"mediaScene": "video",
"neoInfos": [neo_info],
"pageId": material.page_id,
"posId": material.pos_id,
"reportType": 0,
"sessionId": session_id,
"startTime": start_time,
"subPageId": material.sub_page_id,
}
class KsNebulaClient:
def __init__(
self,
*,
full_cookie: str,
client_salt: str,
cookie_dict: dict[str, str],
timeout: int,
dry_run: bool,
memory_device: bool = False,
device_online: bool = True,
device_seed: int | None = None,
device_low_coin_threshold: int = 0,
device_max_switches: int = 0,
rotate_h5_device: bool = False,
strict_device_online: bool = False,
) -> None:
self.full_cookie = full_cookie
self.client_salt = client_salt
self.cookie_dict = cookie_dict
self.h5_cookie_dict = normalize_h5_cookie_wire_values(cookie_dict)
self.h5_full_cookie = cookie_to_string(self.h5_cookie_dict)
self.timeout = timeout
self.dry_run = dry_run
self.ad_materials: dict[str, AdMaterial] = {}
self.ad_fetch_neo_params: dict[str, str] = {}
self.ad_fetch_scenes: dict[str, tuple[int, int, int]] = {}
self.ad_fetch_business: dict[str, int] = {}
self.ad_task_countdown_ms: dict[str, int] = {}
self.ad_task_countdown_toast: dict[str, str] = {}
self.memory_device = memory_device
self.device_online = device_online
self.device_generator = DeviceProfileGenerator(seed=device_seed)
self.device_low_coin_threshold = device_low_coin_threshold
self.device_max_switches = device_max_switches
self.rotate_h5_device = rotate_h5_device
self.strict_device_online = strict_device_online
self.device_switch_count = 0
self.device_profile: DeviceProfile | None = None
self.h5_sig3_counter = ((int(time.time() * 1000) >> 3) & 0xFF) or 160
self.h5_sig3_mode = os.environ.get("KS_H5_SIG3_MODE", "pure").strip().lower()
self.h5_sig3_runner = Path(os.environ.get("KS_H5_SIG3_RUNNER", "core/h5_vendor_encode.mjs"))
self.h5_sig3_server = Path(os.environ.get("KS_H5_SIG3_SERVER", "core/h5_vendor_encode_server.mjs"))
self.h5_sig3_node = os.environ.get("NODE_BIN") or None
self.h5_encoder: H5JsBridgeEncoder | None = None
self._h5_sig3_warned = False
self.h5_kww_mode = os.environ.get("KS_H5_KWW_MODE", "pure").strip().lower()
self.h5_kww_server = Path(os.environ.get("KS_H5_KWW_SERVER", "core/h5_kww_server.mjs"))
self.h5_kww_node = os.environ.get("NODE_BIN") or None
self.h5_kww_generator: H5KwwGenerator | PureH5KwwGenerator | None = None
self._h5_kww_warned = False
self._api_http_session: Any | None = None
self._api_weapon_header_provider = WeaponProofProvider(API_HEADERS["kaw"])
self.sign_in_event_tracking = SIGN_IN_EVENT_TRACKING
self.treasure_open_event_tracking = _compact_json(TREASURE_OPEN_EVENT_TRACKING)
if self.memory_device:
self.register_memory_device("startup")
@classmethod
def from_env(
cls,
*,
timeout: int,
dry_run: bool,
device_profile_path: str | Path | None = None,
memory_device: bool = False,
device_online: bool = True,
device_seed: int | None = None,
device_low_coin_threshold: int = 0,
device_max_switches: int = 0,
rotate_h5_device: bool = False,
strict_device_online: bool = False,
) -> "KsNebulaClient":
full_cookie, client_salt, cookie_dict = cookie_from_env(device_profile_path=device_profile_path)
return cls(
full_cookie=full_cookie,
client_salt=client_salt,
cookie_dict=cookie_dict,
timeout=timeout,
dry_run=dry_run,
memory_device=memory_device,
device_online=device_online,
device_seed=device_seed,
device_low_coin_threshold=device_low_coin_threshold,
device_max_switches=device_max_switches,
rotate_h5_device=rotate_h5_device,
strict_device_online=strict_device_online,
)
def _apply_device_profile(self, profile: DeviceProfile) -> None:
self.cookie_dict = apply_device_profile_to_cookie(self.cookie_dict, profile)
self.full_cookie = cookie_to_string(self.cookie_dict)
if self.rotate_h5_device:
self.h5_cookie_dict = normalize_h5_cookie_wire_values(apply_device_profile_to_cookie(self.h5_cookie_dict, profile))
self.h5_full_cookie = cookie_to_string(self.h5_cookie_dict)
def register_memory_device(self, reason: str, *, hard_fail: bool = True) -> DeviceProfile | None:
profile = self.device_generator.new_profile()
local_profile = DeviceProfile.from_dict(profile.to_dict())
online_accepted = False
if self.device_online and not self.dry_run:
try:
result = run_online_bootstrap(profile, timeout=self.timeout)
identity = result.get("identity", {}) if isinstance(result, dict) else {}
if not identity.get("egid"):
raise RuntimeError("DFP 注册未返回 EGID")
online_accepted = True
except Exception as exc:
if self.strict_device_online:
if hard_fail:
raise SystemExit(
f"[设备] DFP 在线注册失败,拒绝 fallback 假 EGID reason={reason} error={exc}"
"(如需离线本地画像,请加 --no-device-online"
)
print(f"[设备] DFP 在线注册失败,保留当前设备继续 reason={reason} error={exc}")
return self.device_profile
profile = local_profile
print(f"[设备] DFP 注册失败,继续使用本地画像 reason={reason} error={exc}")
self._apply_device_profile(profile)
self.device_profile = profile
mode = "服务端设备" if online_accepted else "内存设备"
print(f"[设备] 使用{mode} reason={reason} did={profile.did} egid={profile.egid or '-'}")
return profile
def maybe_rotate_device_for_record(self, record: ResponseRecord) -> bool:
if not self.memory_device:
return False
if self.device_low_coin_threshold <= 0:
return False
if self.device_max_switches <= 0 or self.device_switch_count >= self.device_max_switches:
return False
if not record.ok:
return False
reason = "low_coin"
if record.label.endswith("_ad_report"):
amount = extract_neo_amount(record.response_json)
elif record.label.endswith("_ad_skip") and isinstance(record.response_json, dict):
if record.response_json.get("reason") != "low_expected_reward":
return False
try:
amount = int(record.response_json.get("expectedReward") or 0)
except (TypeError, ValueError):
amount = None
reason = "low_expected_reward"
else:
return False
if amount is None or amount > self.device_low_coin_threshold:
return False
self.device_switch_count += 1
self.register_memory_device(f"{reason}:{amount}", hard_fail=False)
return True
def _cookie_header(self) -> str:
return self.full_cookie
def _h5_cookie_header(self) -> str:
return self.h5_full_cookie
def _ensure_h5_kws_cookie_fields(self, *, url: str = "") -> None:
"""补齐 WebWeapon/KWS 票据字段。"""
product_name = self.h5_cookie_dict.get("kwpsecproductname") or KWS_PRODUCT_NAME
self.h5_cookie_dict["kwpsecproductname"] = product_name
if not (self.h5_cookie_dict.get("kwssectoken") and self.h5_cookie_dict.get("kwscode")):
# kwssectoken 与 kwscode 是成对生成的;任一缺失时整对重算,
# 避免旧 token 搭配新 code 造成服务端校验上下文不一致。
kws_mode = os.environ.get("KS_H5_KWS_MODE", "").strip().lower()
server_sec_token = (
os.environ.get("KS_H5_KWS_SEC_TOKEN")
or os.environ.get("KWS_SEC_TOKEN")
or ""
).strip()
if server_sec_token and kws_mode in {"script", "sign", "server", "config"}:
self.h5_cookie_dict.update(
build_h5_kws_script_ticket(
sec_token=server_sec_token,
product_name=product_name,
)
)
else:
ticket = build_h5_kws_default_ticket(
url=url,
did=self.h5_cookie_dict.get("did") or self.cookie_dict.get("did", ""),
product_name=product_name,
)
self.h5_cookie_dict["kwssectoken"] = ticket.kwssectoken
self.h5_cookie_dict["kwscode"] = ticket.kwscode
if not self.h5_cookie_dict.get("kwfv1"):
self.h5_cookie_dict["kwfv1"] = ticket.kwfv1
self.h5_full_cookie = cookie_to_string(self.h5_cookie_dict)
def _api_cookie_header(self) -> str:
api_st = self.cookie_dict.get("kuaishou.api_st", "")
return f"kuaishou.api_st={api_st}" if api_st else self.full_cookie
def _device_oaid(self) -> str:
return (
os.environ.get("KS_OAID")
or self.cookie_dict.get("oaid", "")
or (self.device_profile.runtime_hints.oaid if self.device_profile else "")
or DEFAULT_OAID
)
def _h5_device_oaid(self) -> str:
return os.environ.get("KS_H5_OAID") or self.h5_cookie_dict.get("oaid", "") or DEFAULT_OAID
def _task_list_params(self) -> dict[str, str]:
params = dict(TASK_LIST_PARAMS)
params["oaid"] = self._h5_device_oaid()
return params
def _treasure_open_body(self) -> str:
return treasure_open_body_for_oaid(self._h5_device_oaid(), self.treasure_open_event_tracking)
def _api_params(self) -> dict[str, str]:
params = dict(API_BASE_PARAMS)
cookie_map = {
"ud": self.cookie_dict.get("ud") or self.cookie_dict.get("userId") or "",
"did": self.cookie_dict.get("did", ""),
"did_tag": self.cookie_dict.get("did_tag", ""),
"cdid_tag": self.cookie_dict.get("cdid_tag", ""),
"egid": self.cookie_dict.get("egid", ""),
"oDid": self.cookie_dict.get("oDid", ""),
"rdid": self.cookie_dict.get("rdid", ""),
"mod": self.cookie_dict.get("mod", params.get("mod", "")),
"deviceName": self.cookie_dict.get("deviceName", params.get("deviceName", "")),
"appver": self.cookie_dict.get("appver", params.get("appver", "")),
"kpn": self.cookie_dict.get("kpn", "NEBULA"),
"kpf": self.cookie_dict.get("kpf", "ANDROID_PHONE"),
"c": self.cookie_dict.get("c", params.get("c", "")),
"oc": self.cookie_dict.get("oc", params.get("oc", "")),
"newOc": self.cookie_dict.get("newOc", params.get("newOc", "")),
"sys": self.cookie_dict.get("sys", params.get("sys", "")),
"net": self.cookie_dict.get("net", params.get("net", "")),
"isp": self.cookie_dict.get("isp", params.get("isp", "")),
"ver": self.cookie_dict.get("ver", params.get("ver", "")),
"did_gt": self.cookie_dict.get("did_gt", params.get("did_gt", "")),
"boardPlatform": self.cookie_dict.get("boardPlatform", params.get("boardPlatform", "")),
"socName": self.cookie_dict.get("socName", params.get("socName", "")),
"max_memory": self.cookie_dict.get("max_memory", params.get("max_memory", "")),
"deviceBit": self.cookie_dict.get("deviceBit", params.get("deviceBit", "")),
"sw": self.cookie_dict.get("sw", params.get("sw", "")),
"sh": self.cookie_dict.get("sh", params.get("sh", "")),
"sbh": self.cookie_dict.get("sbh", params.get("sbh", "")),
"ddpi": self.cookie_dict.get("ddpi", params.get("ddpi", "")),
"totalMemory": self.cookie_dict.get("totalMemory", params.get("totalMemory", "")),
"abi": self.cookie_dict.get("abi", params.get("abi", "")),
"device_abi": self.cookie_dict.get("device_abi", params.get("device_abi", "")),
"cold_launch_time_ms": self.cookie_dict.get("cold_launch_time_ms", params.get("cold_launch_time_ms", "")),
"sid": self.cookie_dict.get("sid", params.get("sid", "")),
"country_code": self.cookie_dict.get("country_code", params.get("country_code", "")),
"androidApiLevel": self.cookie_dict.get("androidApiLevel", params.get("androidApiLevel", "")),
}
for key, value in cookie_map.items():
params[key] = value
return params
def _api_body_common(self) -> list[tuple[str, str]]:
api_st = self.cookie_dict.get("kuaishou.api_st", "")
token = self.cookie_dict.get("token") or api_st
pairs = list(API_COMMON_BODY.items())
pairs.append(("kuaishou.api_st", api_st))
pairs.append(("token", token))
return pairs
def _update_ad_fetch_contexts(self, data: Any) -> None:
if not isinstance(data, dict):
return
root = data.get("data") if isinstance(data.get("data"), dict) else data
if not isinstance(root, dict):
return
tasks: list[Any] = []
for key in ("dailyTasks", "flagTaskList", "finishedTaskList"):
value = root.get(key)
if isinstance(value, list):
tasks.extend(value)
kind_by_business = {605: "sign_in", 606: "treasure", 672: "normal"}
for task in tasks:
if not isinstance(task, dict):
continue
link_url = str(task.get("linkUrl") or "")
decoded = decode_neo_params(link_url)
business_id = safe_int(decoded.get("businessId"), 0)
kind = kind_by_business.get(business_id)
if not kind:
continue
fallback_scene = AD_FETCH_FALLBACK_SCENE[kind]
self.ad_fetch_neo_params[kind] = link_url
self.ad_fetch_scenes[kind] = (
safe_int(decoded.get("pageId"), fallback_scene[0]),
safe_int(decoded.get("subPageId"), fallback_scene[1]),
safe_int(decoded.get("posId"), fallback_scene[2]),
)
self.ad_fetch_business[kind] = business_id
ext_param = task.get("extParam")
countdown_ms = 0
toast = ""
if isinstance(ext_param, dict):
countdown_ms = safe_int(ext_param.get("descCountDown"), 0)
toast = str(ext_param.get("buttonCountDownToast") or "")
self.ad_task_countdown_ms[kind] = max(0, countdown_ms)
self.ad_task_countdown_toast[kind] = toast
def _update_h5_state_contexts(self, data: Any) -> None:
if not isinstance(data, dict):
return
root = data.get("data") if isinstance(data.get("data"), dict) else data
if not isinstance(root, dict):
return
treasure_event = event_tracking_with_source(root.get("eventTrackingLogInfo"))
if treasure_event:
self.treasure_open_event_tracking = treasure_event
tasks: list[Any] = []
for key in ("dailyTasks", "flagTaskList", "finishedTaskList"):
value = root.get(key)
if isinstance(value, list):
tasks.extend(value)
for task in tasks:
if not isinstance(task, dict):
continue
link_url = str(task.get("linkUrl") or "")
event = task.get("eventTrackingLogInfo")
event_task_id = safe_int(task.get("id") or task.get("taskId"), 0)
if isinstance(event, dict):
event_task_id = safe_int(event.get("eventTrackingTaskId"), event_task_id)
is_sign_in_report = (
"/rest/wd/encourage/unionTask/signIn/report" in link_url
or event_task_id == 20022
)
if not is_sign_in_report:
continue
sign_event = ""
if isinstance(event, dict) and event.get("resourceId") == "externalFeed_externalSignPopup":
sign_event = event_tracking_with_source(event)
if not sign_event:
ext_param = task.get("extParam")
if isinstance(ext_param, dict):
ext_event = ext_param.get("eventTrackingLogInfo")
if isinstance(ext_event, dict) and ext_event.get("resourceId") == "externalFeed_externalSignPopup":
sign_event = event_tracking_with_source(ext_event)
if sign_event:
self.sign_in_event_tracking = sign_event
return
def _request(self, label: str, title: str, request_id: int, method: str, url: str, headers: dict[str, str], body: bytes | None = None) -> ResponseRecord:
headers = dict(headers)
headers.setdefault("Accept-Encoding", "gzip")
if "Cookie" not in headers:
headers["Cookie"] = self._cookie_header()
if self.dry_run:
return ResponseRecord(label, title, request_id, None, True, url, {
"dryRun": True,
"method": method,
"host": urllib.parse.urlsplit(url).netloc,
"path": urllib.parse.urlsplit(url).path,
"bodySize": len(body or b""),
}, "")
attempts = max(1, int(os.environ.get("KS_HTTP_RETRIES", "3")))
backoff = max(0.0, float(os.environ.get("KS_RATE_LIMIT_BACKOFF", "8")))
for attempt in range(attempts):
attempt_headers = dict(headers)
is_api_request = urllib.parse.urlsplit(url).netloc == API_HOST
if is_api_request:
attempt_headers["X-REQUESTID"] = generate_request_id()
attempt_headers.update(self._api_weapon_header_provider(url))
try:
if is_api_request:
if self._api_http_session is None:
self._api_http_session = create_http_session("okhttp4-android10")
resp = self._api_http_session.request(
method,
url,
data=body if method not in {"GET", "HEAD"} else None,
headers=attempt_headers,
timeout=self.timeout,
)
status = int(resp.status_code)
text = decode_response(
bytes(resp.content),
resp.headers.get("Content-Encoding", ""),
)
data = parse_json(text)
error = "" if 200 <= status < 300 else f"http_status_{status}"
record = ResponseRecord(
label,
title,
request_id,
status,
is_business_ok(status, data),
url,
data,
text,
error,
)
else:
req = urllib.request.Request(
url,
data=body if method not in {"GET", "HEAD"} else None,
headers=attempt_headers,
method=method,
)
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
text = decode_response(resp.read(), resp.headers.get("Content-Encoding", ""))
data = parse_json(text)
record = ResponseRecord(label, title, request_id, resp.status, is_business_ok(resp.status, data), url, data, text)
except urllib.error.HTTPError as exc:
text = decode_response(exc.read(), exc.headers.get("Content-Encoding", ""))
data = parse_json(text)
record = ResponseRecord(label, title, request_id, exc.code, False, url, data, text, f"http_status_{exc.code}")
except Exception as exc:
record = ResponseRecord(label, title, request_id, None, False, url, None, "", str(exc))
if attempt < attempts - 1 and is_rate_limited(record.status, record.response_json):
delay = backoff * (attempt + 1)
if delay > 0:
print(f"{title} 触发限速,等待 {delay:.1f}s 后重试 ...")
time.sleep(delay)
continue
return record
return record
def _h5_get(self, label: str, title: str, request_id: int, path: str, params: dict[str, str], header_key: str) -> ResponseRecord:
url = f"{NEBULA_BASE}{path}?{urllib.parse.urlencode(params)}"
return self._request(label, title, request_id, "GET", url, self._h5_headers(header_key, url=url, method="GET"))
def _h5_headers(self, header_key: str, *, url: str = "", method: str = "GET", body: str = "") -> dict[str, str]:
headers = dict(H5_HEADERS[header_key])
headers["Cookie"] = self._h5_cookie_header()
if "kww" in headers:
self._ensure_h5_kws_cookie_fields(url=url or headers.get("Referer", ""))
headers["Cookie"] = self._h5_cookie_header()
if "kww" in headers and self.h5_kww_mode not in {"0", "off", "static", "disabled"}:
try:
# KWF WebView 会同时维护 header.kww 与 localStorage/cookie
# 里的 kwfv1/kwfcv1。这里让纯算链路保持同样状态避免
# ksck 只带基础登录态时 H5 Cookie 缺少动态风控字段。
if self.h5_kww_generator is None:
if self.h5_kww_mode in {"pure", "python", "py"}:
self.h5_kww_generator = PureH5KwwGenerator()
else:
self.h5_kww_generator = H5KwwGenerator(
server=self.h5_kww_server,
node_bin=self.h5_kww_node,
timeout=self.timeout,
)
result = self.h5_kww_generator.get(
url=url,
method=method,
headers={key: value for key, value in headers.items() if key.lower() != "cookie"},
body=body,
cookie=self._h5_cookie_header(),
)
headers["kww"] = result.kww
if result.kwfv1:
self.h5_cookie_dict["kwfv1"] = result.kwfv1
if result.kwfcv1:
self.h5_cookie_dict["kwfcv1"] = result.kwfcv1
self.h5_full_cookie = cookie_to_string(self.h5_cookie_dict)
headers["Cookie"] = self._h5_cookie_header()
except Exception as exc:
strict = self.h5_kww_mode in {"strict", "dynamic_strict"} or os.environ.get("KS_H5_KWW_STRICT") == "1"
if strict:
raise
if self.h5_kww_generator is not None:
self.h5_kww_generator.close()
self.h5_kww_generator = None
if not self._h5_kww_warned:
print(f"[H5] 动态 kww 不可用,回退静态 header: {exc}")
self._h5_kww_warned = True
return headers
def _next_h5_sig3(
self,
kind: str,
query: dict[str, Any] | None = None,
body: str | dict[str, Any] | list[Any] | None = None,
method: str = "GET",
request_type: str = "json",
) -> str:
if self.h5_sig3_mode in {"jsbridge", "node", "vm", "jsbridge_strict", "strict"}:
try:
if self.h5_encoder is None:
self.h5_encoder = H5JsBridgeEncoder(
server=self.h5_sig3_server,
node_bin=self.h5_sig3_node,
timeout=self.timeout,
)
result = h5_sig3_for_request(
self.h5_cookie_dict,
query=query,
body=body,
method=method,
request_type=request_type,
runner=self.h5_sig3_runner,
node_bin=self.h5_sig3_node,
timeout=self.timeout,
encoder=self.h5_encoder,
)
return result.sig3
except Exception as exc:
if self.h5_encoder is not None:
self.h5_encoder.close()
self.h5_encoder = None
strict = self.h5_sig3_mode in {"jsbridge_strict", "strict"} or os.environ.get("KS_H5_SIG3_STRICT") == "1"
if strict:
raise
if not self._h5_sig3_warned:
print(f"[H5] JSBridge sig3 不可用,回退字段生成: {exc}")
self._h5_sig3_warned = True
if self.h5_sig3_mode in {"fields", "static"}:
crc32_value = H5_SIG3_CRC32[kind]
else:
sign_input = build_h5_sign_input(
self.h5_cookie_dict,
query=query,
body=body,
method=method,
request_type=request_type,
)
crc32_value = h5_sig3_crc32_from_sign_input(sign_input)
if self.h5_sig3_counter <= 1:
self.h5_sig3_counter = 160
self.h5_sig3_counter -= 1
elapsed_ms = 0x4B4B0000 | (int(time.monotonic() * 1000) & 0xFFFF)
state_seed = (
f"{kind}:{self.h5_sig3_counter}:{elapsed_ms}:{time.time_ns()}:"
f"{self.h5_cookie_dict.get('userId', '')}:{self.h5_cookie_dict.get('did', '')}"
).encode("utf-8")
state_value = 0x10000 | int.from_bytes(hashlib.sha256(state_seed).digest()[:2], "little")
return h5_sig3_from_fields(
crc32_value=crc32_value,
counter=self.h5_sig3_counter,
elapsed_ms=elapsed_ms,
state_value=state_value,
)
def query_balance(self) -> ResponseRecord:
return self._h5_get("login_check", "登录态/余额检查", 1286, "/rest/n/nebula/activity/earn/overview/basicInfo", {"source": SOURCE}, "login")
def final_balance(self) -> ResponseRecord:
return self._h5_get("final_balance", "最终余额", 2709, "/rest/n/nebula/activity/earn/overview/basicInfo", {"source": SOURCE}, "login")
def task_list(self) -> ResponseRecord:
record = self._h5_get("task_list", "任务列表", 1383, "/rest/n/nebula/activity/earn/overview/tasks", self._task_list_params(), "task_list")
self._update_ad_fetch_contexts(record.response_json)
self._update_h5_state_contexts(record.response_json)
return record
def sign_in_resource(self) -> ResponseRecord:
return self._h5_get(
"sign_in_resource",
"签到资源",
1476,
"/rest/wd/encourage/unionTask/signIn/resource",
{"__NS_sig3": self._next_h5_sig3("sign_in_resource"), "sigCatVer": "1"},
"sign_in_resource",
)
def sign_in(self) -> ResponseRecord:
if os.environ.get("KS_SIGNED_STATE_TASKS", "0") == "1":
return self.sign_in_api()
query = {"eventTrackingLogInfo": self.sign_in_event_tracking}
record = self._h5_get(
"sign_in_report",
"签到上报",
1633,
"/rest/wd/encourage/unionTask/signIn/report",
{"__NS_sig3": self._next_h5_sig3("sign_in_report", query=query), "sigCatVer": "1", **query},
"sign_in_report",
)
return record
def sign_in_api(self) -> ResponseRecord:
path = "/rest/n/encourage/unionTask/signIn/report"
body_pairs = [("eventTrackingLogInfo", "")] + self._api_body_common()
url = signed_api_url(path, self._api_params(), body_pairs, self.client_salt, API_SIG3_SEED["sign_in_report"])
body = urllib.parse.urlencode(body_pairs).encode("utf-8")
headers = dict(API_HEADERS)
headers["Cookie"] = self._api_cookie_header()
return self._request("sign_in_report", "签到上报(API)", 1633, "POST", url, headers, body)
def treasure_box_info(self) -> ResponseRecord:
query = {"source": SOURCE}
record = self._h5_get(
"treasure_info",
"宝箱信息",
1376,
"/rest/wd/encourage/unionTask/treasureBox/info",
{"__NS_sig3": self._next_h5_sig3("treasure_info", query=query), "sigCatVer": "1", **query},
"treasure_info",
)
self._update_h5_state_contexts(record.response_json)
return record
def open_treasure_box(self) -> ResponseRecord:
if os.environ.get("KS_SIGNED_STATE_TASKS", "0") == "1":
return self.open_treasure_box_api()
body_text = self._treasure_open_body()
sig3 = self._next_h5_sig3("treasure_open", body=body_text, method="POST", request_type="json")
url = f"{NEBULA_BASE}/rest/wd/encourage/unionTask/treasureBox/report?{urllib.parse.urlencode({"__NS_sig3": sig3, "sigCatVer": "1"})}"
body = body_text.encode("utf-8")
record = self._request(
"treasure_open",
"开宝箱",
2105,
"POST",
url,
self._h5_headers("treasure_open", url=url, method="POST", body=body_text),
body,
)
return record
def open_treasure_box_api(self) -> ResponseRecord:
path = "/rest/n/encourage/unionTask/treasureBox/report"
body_pairs = [("layoutPage", "NEBULA_ACTIVITY"), ("eventTrackingLogInfo", "")] + self._api_body_common()
url = signed_api_url(path, self._api_params(), body_pairs, self.client_salt, API_SIG3_SEED["treasure_report"])
body = urllib.parse.urlencode(body_pairs).encode("utf-8")
headers = dict(API_HEADERS)
headers["Cookie"] = self._api_cookie_header()
return self._request("treasure_open", "开宝箱(API)", 2105, "POST", url, headers, body)
def fetch_ad(self, kind: str) -> ResponseRecord:
path = "/rest/e/reward/mixed/ad"
api_params = self._api_params()
scene = self.ad_fetch_scenes.get(kind, AD_FETCH_FALLBACK_SCENE[kind])
business_id = self.ad_fetch_business.get(kind, AD_FETCH_FALLBACK_BUSINESS[kind])
str_e = build_reward_str_e(
kind=kind,
cookie_dict=self.cookie_dict,
api_params=api_params,
oaid=self._device_oaid(),
scene=scene,
business_id=business_id,
neo_params=self.ad_fetch_neo_params.get(kind, ""),
now_ms=int(time.time() * 1000),
network_ip=os.environ.get("KS_AD_NETWORK_IP", "172.31.230.147"),
ipdx_ip=os.environ.get("KS_AD_IPDX_IP", "106.178.190.155"),
)
state = Kwsg10418State.from_digest(API_SIG3_SEED[f"{kind}_fetch"])
t1, t2 = enc_tables()
reward_body = build_reward_body(str_e, state, t1=t1, t2=t2)
body_pairs = [("encData", reward_body.enc_data), ("sign", reward_body.sign)] + self._api_body_common()
url = signed_api_url(path, api_params, body_pairs, self.client_salt, API_SIG3_SEED[f"{kind}_fetch"], state=state)
body = urllib.parse.urlencode(body_pairs).encode("utf-8")
headers = dict(API_HEADERS)
headers["Cookie"] = self._api_cookie_header()
record = self._request(f"{kind}_ad_fetch", {"sign_in": "签到广告拉取", "treasure": "宝箱广告拉取", "normal": "看广告得金币拉取"}[kind], {"sign_in": 1688, "treasure": 2119, "normal": 2637}[kind], "POST", url, headers, body)
material = extract_ad_material(kind, record.response_json)
if material is not None:
self.ad_materials[kind] = material
return record
def report_ad(self, kind: str) -> ResponseRecord:
material = self.ad_materials.get(kind)
if material is None:
material = fallback_ad_material(kind) if self.dry_run else None
label = f"{kind}_ad_report"
title = {"sign_in": "签到广告上报", "treasure": "宝箱广告上报", "normal": "看广告得金币上报"}[kind]
request_id = {"sign_in": 2056, "treasure": 2418, "normal": 2700}[kind]
if material is None:
return ResponseRecord(label, title, request_id, None, False, "", None, "", "missing_ad_material")
biz = build_ad_report_biz(material, self.cookie_dict)
biz_str = json.dumps(biz, ensure_ascii=False, separators=(",", ":"))
mode = os.environ.get("KS_REPORT_BODY_MODE", "encData").lower()
first_pair = ("bizStr", biz_str) if mode == "bizstr" else ("encData", build_report_enc_data(biz_str.encode("utf-8")))
body_pairs = [first_pair] + self._api_body_common()
path = "/rest/r/ad/task/report"
url = signed_api_url(path, self._api_params(), body_pairs, self.client_salt, API_SIG3_SEED[f"{kind}_report"])
body = urllib.parse.urlencode(body_pairs).encode("utf-8")
headers = dict(API_HEADERS)
headers["Cookie"] = self._api_cookie_header()
return self._request(label, title, request_id, "POST", url, headers, body)
def _wait_then_report(self, kind: str, ad_wait: float) -> ResponseRecord:
if not self.dry_run:
wait_seconds = ad_wait
if ad_wait < 0:
material = self.ad_materials.get(kind)
wait_seconds = (material.material_time / 1000.0) if material else 0
if wait_seconds > 0:
print(f"等待广告模拟观看 {wait_seconds:.1f}s ...")
time.sleep(wait_seconds)
return self.report_ad(kind)
def _ad_skip_record(self, kind: str, reason: str, payload: dict[str, Any]) -> ResponseRecord:
title = {"sign_in": "签到广告跳过", "treasure": "宝箱广告跳过", "normal": "看广告得金币跳过"}[kind]
request_id = {"sign_in": 1688, "treasure": 2119, "normal": 2637}[kind]
data = {"skipped": True, "reason": reason}
data.update(payload)
return ResponseRecord(f"{kind}_ad_skip", title, request_id, None, True, "", data, "")
def run_ad_sequence(self, kind: str, ad_wait: float, skip_reward_leq: int = 0) -> list[ResponseRecord]:
countdown_ms = self.ad_task_countdown_ms.get(kind, 0)
if kind == "normal" and countdown_ms > 0:
return [
self._ad_skip_record(
kind,
"task_countdown",
{
"countdownMs": countdown_ms,
"toast": self.ad_task_countdown_toast.get(kind, ""),
},
)
]
fetch_record = self.fetch_ad(kind)
records = [fetch_record]
if not fetch_record.ok:
return records
if kind not in self.ad_materials and not self.dry_run:
return records
expected_reward = extract_fetch_expected_reward(fetch_record.response_json)
if skip_reward_leq > 0 and 0 < expected_reward <= skip_reward_leq:
records.append(self._ad_skip_record(kind, "low_expected_reward", {"expectedReward": expected_reward}))
return records
records.append(self._wait_then_report(kind, ad_wait))
return records
def run_all_once(
self,
*,
ad_wait: float,
out_dir: Path,
normal_ad_count: int = 1,
skip_ad_reward_leq: int = 0,
) -> int:
if normal_ad_count < 0:
raise SystemExit("--normal-ad-count must be >= 0")
if skip_ad_reward_leq < 0:
raise SystemExit("--skip-ad-reward-leq must be >= 0")
out_dir.mkdir(parents=True, exist_ok=True)
record_path = out_dir / f"request_replay_{time.strftime('%Y%m%d_%H%M%S')}.jsonl"
def run_ad_action(kind: str) -> list[ResponseRecord]:
if skip_ad_reward_leq:
return self.run_ad_sequence(kind, ad_wait, skip_reward_leq=skip_ad_reward_leq)
return self.run_ad_sequence(kind, ad_wait)
actions = [
self.query_balance,
self.task_list,
self.sign_in_resource,
self.sign_in,
lambda: run_ad_action("sign_in"),
self.treasure_box_info,
self.open_treasure_box,
lambda: run_ad_action("treasure"),
*[lambda: run_ad_action("normal") for _ in range(normal_ad_count)],
self.final_balance,
]
failures = 0
request_count = 0
for action in actions:
result = action()
records = result if isinstance(result, list) else [result]
for record in records:
request_count += 1
with record_path.open("a", encoding="utf-8") as fp:
fp.write(json.dumps(record.to_json(), ensure_ascii=False, separators=(",", ":")) + "\n")
status = "OK" if record.ok else "FAIL"
print(f"[{status}] {record.title} #{record.request_id} {record.summary()}")
if not record.ok:
failures += 1
self.maybe_rotate_device_for_record(record)
if self.h5_encoder is not None:
self.h5_encoder.close()
self.h5_encoder = None
if self.h5_kww_generator is not None:
self.h5_kww_generator.close()
self.h5_kww_generator = None
print("")
print("任务执行完成")
print(f"请求数量: {request_count}")
print(f"失败数量: {failures}")
print(f"记录文件: {record_path}")
return 1 if failures else 0
def run_all(
*,
dry_run: bool,
timeout: int,
ad_wait: float,
out_dir: Path,
device_profile_path: str | Path | None = None,
memory_device: bool = False,
device_online: bool = True,
device_seed: int | None = None,
device_low_coin_threshold: int = 0,
device_max_switches: int = 0,
normal_ad_count: int = 1,
skip_ad_reward_leq: int = 0,
rotate_h5_device: bool = False,
strict_device_online: bool = False,
) -> int:
return KsNebulaClient.from_env(
timeout=timeout,
dry_run=dry_run,
device_profile_path=device_profile_path,
memory_device=memory_device,
device_online=device_online,
device_seed=device_seed,
device_low_coin_threshold=device_low_coin_threshold,
device_max_switches=device_max_switches,
rotate_h5_device=rotate_h5_device,
strict_device_online=strict_device_online,
).run_all_once(
ad_wait=ad_wait,
out_dir=out_dir,
normal_ad_count=normal_ad_count,
skip_ad_reward_leq=skip_ad_reward_leq,
)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="快手极速版任务脚本方法化接口Cookie 从 .env 读取一次)")
parser.add_argument("--dry-run", action="store_true", default=os.environ.get("KS_DRY_RUN", "0") == "1")
parser.add_argument("--timeout", type=int, default=int(os.environ.get("KS_TIMEOUT", "30")))
parser.add_argument("--ad-wait", type=float, default=float(os.environ.get("KS_AD_WAIT_SECONDS", "-1")))
parser.add_argument("--out-dir", default=os.environ.get("KS_OUT_DIR", "out"))
parser.add_argument("--device-profile", default=os.environ.get("KS_DEVICE_PROFILE", ""))
parser.add_argument("--memory-device", action="store_true", default=os.environ.get("KS_MEMORY_DEVICE", "0") == "1")
parser.add_argument("--server-device", action="store_true", default=os.environ.get("KS_SERVER_DEVICE", "0") == "1")
parser.add_argument("--no-device-online", action="store_true", default=os.environ.get("KS_DEVICE_ONLINE", "1") == "0")
parser.add_argument("--device-seed", type=int, default=int(os.environ["KS_DEVICE_SEED"]) if os.environ.get("KS_DEVICE_SEED") else None)
parser.add_argument("--device-low-coin-threshold", type=int, default=int(os.environ.get("KS_DEVICE_LOW_COIN_THRESHOLD", "0")))
parser.add_argument("--device-max-switches", type=int, default=int(os.environ.get("KS_DEVICE_MAX_SWITCHES", "3")))
parser.add_argument("--normal-ad-count", type=int, default=int(os.environ.get("KS_NORMAL_AD_COUNT", "1")))
parser.add_argument("--skip-ad-reward-leq", type=int, default=int(os.environ.get("KS_SKIP_AD_REWARD_LEQ", "0")))
parser.add_argument("--rotate-h5-device", action="store_true", default=os.environ.get("KS_ROTATE_H5_DEVICE", "0") == "1")
parser.add_argument("--strict-device-online", action="store_true", default=os.environ.get("KS_STRICT_DEVICE_ONLINE", "0") == "1")
parser.add_argument("--print-algorithms", action="store_true", default=os.environ.get("KS_PRINT_ALGORITHMS", "0") == "1")
return parser
def main() -> int:
load_dotenv()
args = build_parser().parse_args()
if args.print_algorithms:
print_local_algorithm_stack()
device_options = args_to_device_options(args)
return run_all(
dry_run=args.dry_run,
timeout=args.timeout,
ad_wait=args.ad_wait,
out_dir=Path(args.out_dir),
device_profile_path=args.device_profile,
memory_device=device_options.memory_device,
device_online=device_options.device_online,
device_seed=args.device_seed,
device_low_coin_threshold=args.device_low_coin_threshold,
device_max_switches=args.device_max_switches,
normal_ad_count=args.normal_ad_count,
skip_ad_reward_leq=args.skip_ad_reward_leq,
rotate_h5_device=device_options.rotate_h5_device,
strict_device_online=device_options.strict_device_online,
)
if __name__ == "__main__":
raise SystemExit(main())