686 lines
23 KiB
Python
686 lines
23 KiB
Python
"""快手极速版短信登录 CLI(全新设备 -> 注册 -> 发码 -> 验码 -> 会话)。
|
||
|
||
每次执行都会现场生成设备画像并在线注册,不读取或保存历史设备、抓包字段和
|
||
区域票据。公开参数只包含登录输入和 HTTP 传输选择:
|
||
|
||
uv run python -m tools.sms_login_cli --mobile 13800000000
|
||
uv run python -m tools.sms_login_cli --mobile 13800000000 --code 123456
|
||
uv run python -m tools.sms_login_cli --mobile 13800000000 --transport okhttp4-android10
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import sys
|
||
import time
|
||
import urllib.parse
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
if str(ROOT) not in sys.path:
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from core.device_cookie import device_profile_cookie_fields # noqa: E402
|
||
from core.device_profile import DeviceProfileGenerator # noqa: E402
|
||
from core.http_transport import create_http_session # noqa: E402
|
||
from core.mobile_encrypt import loginhelper_encrypt_mobile # noqa: E402
|
||
from core.sig3 import Kwsg10418State, kwsg_10418_session_seed_from_time # noqa: E402
|
||
from core.sms_login import ( # noqa: E402
|
||
DEFAULT_LOGIN_HOST,
|
||
DEFAULT_REGION_HOST,
|
||
LOGIN_SMS_CODE_TYPE,
|
||
REGION_FULL_CONFIG_HOST,
|
||
build_account_security_fields,
|
||
login_api_params,
|
||
login_by_code_try_paths,
|
||
refresh_region_ticket,
|
||
request_mobile_code,
|
||
)
|
||
from core.weapon_kas import APK_DEFAULT_WEAPON_KAW, WeaponProofProvider # noqa: E402
|
||
from core.weapon_mf import generate_profile_passport_account_image # noqa: E402
|
||
from tools.new_device import run_online_bootstrap # noqa: E402
|
||
|
||
|
||
LOGIN_COUNTRY_CODE = "+86"
|
||
REQUEST_TIMEOUT_SECONDS = 20
|
||
CAPTCHA_RETRIES = 2
|
||
|
||
_CAPTCHA_RESULT_HINTS = {
|
||
350001: "参数错误:轨迹或 verifyParam 为空/无效",
|
||
350002: "缺口识别或滑动落点错误",
|
||
350005: "verifyParam 中的 captchaSn 错误",
|
||
350009: "验证时间过长,captchaSn 已失效",
|
||
350014: "轨迹或环境校验未通过,需要获取新的 captchaSn",
|
||
350017: "验证参数已失效",
|
||
350029: "服务端已切换验证码类型",
|
||
}
|
||
|
||
_ACCOUNT_COOKIE_ORDER = (
|
||
"kpn",
|
||
"kpf",
|
||
"userId",
|
||
"did",
|
||
"c",
|
||
"ver",
|
||
"appver",
|
||
"language",
|
||
"countryCode",
|
||
"sys",
|
||
"mod",
|
||
"net",
|
||
"deviceName",
|
||
"earphoneMode",
|
||
"isp",
|
||
"ud",
|
||
"did_tag",
|
||
"egid",
|
||
"thermal",
|
||
"kcv",
|
||
"app",
|
||
"bottom_navigation",
|
||
"android_os",
|
||
"oDid",
|
||
"boardPlatform",
|
||
"newOc",
|
||
"androidApiLevel",
|
||
"slh",
|
||
"country_code",
|
||
"nbh",
|
||
"hotfix_ver",
|
||
"did_gt",
|
||
"keyconfig_state",
|
||
"cdid_tag",
|
||
"max_memory",
|
||
"sid",
|
||
"cold_launch_time_ms",
|
||
"oc",
|
||
"sh",
|
||
"deviceBit",
|
||
"browseType",
|
||
"ddpi",
|
||
"socName",
|
||
"is_background",
|
||
"sw",
|
||
"ftt",
|
||
"apptype",
|
||
"abi",
|
||
"cl",
|
||
"userRecoBit",
|
||
"device_abi",
|
||
"icaver",
|
||
"totalMemory",
|
||
"grant_browse_type",
|
||
"iuid",
|
||
"rdid",
|
||
"sbh",
|
||
"darkMode",
|
||
"oaid",
|
||
"client_key",
|
||
"kuaishou.api_st",
|
||
"token",
|
||
"region_ticket",
|
||
"__NSWJ",
|
||
"kuaishou.h5_st",
|
||
"os",
|
||
)
|
||
|
||
|
||
def _mobile_argument(value: str) -> str:
|
||
mobile = value.strip()
|
||
if not mobile.isdigit() or not 5 <= len(mobile) <= 20:
|
||
raise argparse.ArgumentTypeError("手机号必须是 5-20 位数字,不能使用 MOBILE 等占位符")
|
||
return mobile
|
||
|
||
|
||
def build_parser() -> argparse.ArgumentParser:
|
||
parser = argparse.ArgumentParser(
|
||
description="快手极速版短信登录(每次使用全新设备)"
|
||
)
|
||
parser.add_argument(
|
||
"--mobile",
|
||
required=True,
|
||
type=_mobile_argument,
|
||
help="手机号(仅数字,不含国家码)",
|
||
)
|
||
parser.add_argument("--code", default="", help="短信验证码;不传则发码后交互输入")
|
||
parser.add_argument(
|
||
"--base-url",
|
||
default="",
|
||
help="登录 host(默认 apissl.ksapisrv.com)",
|
||
)
|
||
parser.add_argument(
|
||
"--transport",
|
||
choices=("requests", "okhttp4-android10"),
|
||
default="requests",
|
||
help="HTTP 传输(默认 requests)",
|
||
)
|
||
return parser
|
||
|
||
|
||
def _new_sig3_state() -> Kwsg10418State:
|
||
seed = kwsg_10418_session_seed_from_time(int(time.time()))
|
||
return Kwsg10418State(session_seed=seed, counter=0x5F)
|
||
|
||
|
||
def _registrable_domain(host: str) -> str:
|
||
host = (host or "").strip().lower()
|
||
if not host:
|
||
return ""
|
||
labels = host.split(".")
|
||
return "." + ".".join(labels[-2:]) if len(labels) >= 2 else "." + host
|
||
|
||
|
||
def _set_region_cookie(session: Any, ticket: str, *, host: str = "") -> None:
|
||
if not ticket:
|
||
return
|
||
domain = _registrable_domain(host)
|
||
if domain:
|
||
session.cookies.set("region_ticket", ticket, domain=domain)
|
||
session.cookies.set("__NSWJ", "", domain=domain)
|
||
return
|
||
session.cookies.set("region_ticket", ticket)
|
||
session.cookies.set("__NSWJ", "")
|
||
|
||
|
||
def _mask_mobile(value: str) -> str:
|
||
mobile = str(value or "")
|
||
if len(mobile) == 11 and mobile.startswith("1") and mobile.isdigit():
|
||
return f"{mobile[:3]}****{mobile[-4:]}"
|
||
return mobile
|
||
|
||
|
||
def _mask_secret(value: Any, *, head: int = 8, tail: int = 6) -> str:
|
||
text = str(value or "")
|
||
if not text:
|
||
return ""
|
||
if len(text) <= head + tail + 6:
|
||
return f"<set len={len(text)}>"
|
||
return f"{text[:head]}...{text[-tail:]}(len={len(text)})"
|
||
|
||
|
||
def _is_secret_key(key: Any) -> bool:
|
||
name = str(key or "").lower()
|
||
return (
|
||
name
|
||
in {
|
||
"kuaishou.api_st",
|
||
"kuaishou.h5_st",
|
||
"kuaishou.api_client_salt",
|
||
"passtoken",
|
||
"quicklogintoken",
|
||
"region_ticket",
|
||
}
|
||
or name.endswith("_st")
|
||
or "token" in name
|
||
or "salt" in name
|
||
or name
|
||
in {"api_st", "h5_st", "pass_token", "quicklogin_token", "client_salt"}
|
||
)
|
||
|
||
|
||
def _looks_like_secret_text(value: str) -> bool:
|
||
text = value.lower()
|
||
return any(
|
||
marker in text
|
||
for marker in (
|
||
"kuaishou.api_st",
|
||
"kuaishou.h5_st",
|
||
"kuaishou.api_client_salt",
|
||
"passtoken",
|
||
"quicklogintoken",
|
||
"pass_token",
|
||
"client_salt",
|
||
)
|
||
)
|
||
|
||
|
||
def _redact_for_display(value: Any, show_secrets: bool = False) -> Any:
|
||
if show_secrets:
|
||
return value
|
||
if isinstance(value, dict):
|
||
return {
|
||
key: _mask_secret(item)
|
||
if _is_secret_key(key)
|
||
else _redact_for_display(item, show_secrets)
|
||
for key, item in value.items()
|
||
}
|
||
if isinstance(value, list):
|
||
return [_redact_for_display(item, show_secrets) for item in value]
|
||
if isinstance(value, str) and _looks_like_secret_text(value):
|
||
return _mask_secret(value, head=20, tail=12)
|
||
return value
|
||
|
||
|
||
def _json_for_display(value: Any, limit: int, *, show_secrets: bool = False) -> str:
|
||
return json.dumps(
|
||
_redact_for_display(value, show_secrets), ensure_ascii=False
|
||
)[:limit]
|
||
|
||
|
||
def _print_raw_response(
|
||
raw: dict[str, Any], *, show_secrets: bool = False, body_limit: int = 400
|
||
) -> None:
|
||
body = raw.get("body") if isinstance(raw.get("body"), dict) else {}
|
||
print(
|
||
f" status={raw.get('status')} result={body.get('result')} "
|
||
f"error_msg={body.get('error_msg')}"
|
||
)
|
||
print(
|
||
f" body: "
|
||
f"{_json_for_display(body, body_limit, show_secrets=show_secrets)}"
|
||
)
|
||
print(
|
||
f" cookies: "
|
||
f"{_json_for_display(raw.get('cookies') or {}, 500, show_secrets=show_secrets)}"
|
||
)
|
||
print(
|
||
f" set_cookie: "
|
||
f"{_json_for_display(raw.get('set_cookie') or [], 500, show_secrets=show_secrets)}"
|
||
)
|
||
if raw.get("error"):
|
||
print(f" error: {raw.get('error')}")
|
||
|
||
|
||
def _captcha_error_url(raw: dict[str, Any]) -> str:
|
||
body = raw.get("body") if isinstance(raw.get("body"), dict) else {}
|
||
if body.get("result") != 705:
|
||
return ""
|
||
error_url = str(body.get("error_url") or "").strip()
|
||
return error_url if error_url.startswith("https://") else ""
|
||
|
||
|
||
def _solve_captcha_http(
|
||
error_url: str, *, session: Any, show_secrets: bool = False
|
||
) -> str:
|
||
print("[验证] 纯 HTTP 求解(ddddocr + Jose + 合成传感器)")
|
||
try:
|
||
try:
|
||
from tools.captcha_solver_http import solve_captcha
|
||
except ImportError:
|
||
from captcha_solver_http import solve_captcha
|
||
except ImportError as exc:
|
||
print(f"[验证] HTTP solver 导入失败: {exc}")
|
||
return ""
|
||
try:
|
||
result = solve_captcha(session=session, error_url=error_url, verbose=True)
|
||
except Exception as exc: # noqa: BLE001
|
||
print(f"[验证] HTTP solver 异常: {exc.__class__.__name__}: {exc}")
|
||
return ""
|
||
raw_result = result.get("raw") if isinstance(result.get("raw"), dict) else {}
|
||
result_code = result.get("result")
|
||
if result_code is None:
|
||
result_code = raw_result.get("result")
|
||
server_desc = result.get("desc") or raw_result.get("desc") or "-"
|
||
token = str(result.get("captcha_token") or "")
|
||
if token:
|
||
shown_token = token if show_secrets else _mask_secret(token, head=10, tail=8)
|
||
print(
|
||
f"[验证] HTTP 求解成功 result={result_code} "
|
||
f"captchaToken={shown_token}"
|
||
)
|
||
else:
|
||
hint = _CAPTCHA_RESULT_HINTS.get(result_code, "未分类响应")
|
||
print(
|
||
f"[验证] HTTP 求解未通过 result={result_code} "
|
||
f"stage={result.get('stage')} desc={server_desc}"
|
||
)
|
||
print(f" 诊断: {hint}")
|
||
if result_code == 350029:
|
||
print(
|
||
f" next_type={result.get('type') or raw_result.get('type') or '-'} "
|
||
f"config_path="
|
||
f"{result.get('configPath') or raw_result.get('configPath') or '-'}"
|
||
)
|
||
return token
|
||
|
||
|
||
def _complete_captcha_handoff(
|
||
error_url: str,
|
||
*,
|
||
stage: str,
|
||
session: Any,
|
||
show_secrets: bool = False,
|
||
) -> str:
|
||
print(f"[验证] {stage} 触发 705")
|
||
print(f" 验证地址: {error_url}")
|
||
token = _solve_captcha_http(
|
||
error_url, session=session, show_secrets=show_secrets
|
||
)
|
||
if token:
|
||
return token
|
||
print("[验证] 纯 HTTP 未拿到 captchaToken,停止重放;不启用浏览器")
|
||
return ""
|
||
|
||
|
||
def _print_weapon_proof_gap(raw: dict[str, Any]) -> bool:
|
||
request_meta = raw.get("request_meta") if isinstance(raw.get("request_meta"), dict) else {}
|
||
if not request_meta or (
|
||
bool(request_meta.get("has_kaw")) and bool(request_meta.get("has_kas"))
|
||
):
|
||
return False
|
||
print(" 当前请求缺少动态 kaw/kas 设备证明;这是与 APP 最终请求的已确认差异")
|
||
return True
|
||
|
||
|
||
def _print_login_failure_diagnosis(
|
||
raw: dict[str, Any], *, passport_account_image: str
|
||
) -> None:
|
||
body = raw.get("body") if isinstance(raw.get("body"), dict) else {}
|
||
result = body.get("result")
|
||
if result == 705:
|
||
print("[诊断] result=705:触发验证/风控,不是 result=50 签名失败")
|
||
_print_weapon_proof_gap(raw)
|
||
if not passport_account_image:
|
||
print(" 当前登录请求未带 passport_account_image")
|
||
print(" 本次为纯在线新设备路径;705 多为号码风控触发交互式验证")
|
||
print(" 自动验证码求解已启用,拿到 captchaToken 后会重放原请求")
|
||
elif result == 50:
|
||
print("[诊断] result=50:签名验证失败;继续对比 query/body/sig/__NS_sig3/__NS_xfalcon")
|
||
|
||
|
||
def _print_session(session: Any) -> None:
|
||
print(f" api_st = {_mask_secret(session.api_st)}")
|
||
print(f" h5_st = {_mask_secret(session.h5_st)}")
|
||
print(f" client_salt = {_mask_secret(session.client_salt)}")
|
||
print(f" user_id = {session.user_id}")
|
||
if session.region_ticket:
|
||
print(f" region = {_mask_secret(session.region_ticket)}")
|
||
print(f" mobile = {_mask_mobile(session.mobile)}")
|
||
if session.pass_token:
|
||
print(f" pass_token = {_mask_secret(session.pass_token)}")
|
||
if session.quicklogin_token:
|
||
print(f" quicklogin = {_mask_secret(session.quicklogin_token)}")
|
||
|
||
|
||
def _account_cookie_fields(profile: Any, session: Any) -> dict[str, str]:
|
||
fields = {
|
||
str(key): "" if value is None else str(value)
|
||
for key, value in login_api_params(profile).items()
|
||
}
|
||
fields.update(device_profile_cookie_fields(profile))
|
||
fields.update(
|
||
{
|
||
"userId": str(session.user_id),
|
||
"ud": str(session.user_id),
|
||
"client_key": "2ac2a76d",
|
||
"kuaishou.api_st": str(session.api_st),
|
||
"token": str(session.api_st),
|
||
"region_ticket": str(session.region_ticket or ""),
|
||
"__NSWJ": "",
|
||
"kuaishou.h5_st": str(session.h5_st or ""),
|
||
"os": "android",
|
||
"cl": "",
|
||
}
|
||
)
|
||
return fields
|
||
|
||
|
||
def _account_cookie_string(profile: Any, session: Any) -> str:
|
||
fields = _account_cookie_fields(profile, session)
|
||
emitted: set[str] = set()
|
||
parts: list[str] = []
|
||
for key in _ACCOUNT_COOKIE_ORDER:
|
||
if key in fields:
|
||
parts.append(f"{key}={fields[key]}")
|
||
emitted.add(key)
|
||
for key in sorted(key for key in fields if key not in emitted):
|
||
parts.append(f"{key}={fields[key]}")
|
||
return "; ".join(parts)
|
||
|
||
|
||
def _quote_env_value(value: str) -> str:
|
||
return value.replace("\\", "\\\\").replace('"', '\\"')
|
||
|
||
|
||
def _print_account_config(profile: Any, session: Any) -> None:
|
||
cookie = _account_cookie_string(profile, session)
|
||
value = f"账号#{cookie}#{session.client_salt}"
|
||
print("\n[配置] 复制下面一行到 .env:")
|
||
print(f'ksck="{_quote_env_value(value)}"')
|
||
|
||
|
||
def _refresh_anonymous_region(profile: Any, session: Any) -> Any:
|
||
region = None
|
||
for region_host in (DEFAULT_REGION_HOST, REGION_FULL_CONFIG_HOST):
|
||
region = refresh_region_ticket(
|
||
profile,
|
||
api_st="",
|
||
client_salt="",
|
||
user_id="",
|
||
base_url=region_host,
|
||
timeout=REQUEST_TIMEOUT_SECONDS,
|
||
get_func=session.get,
|
||
)
|
||
if region.ok:
|
||
break
|
||
if region_host != REGION_FULL_CONFIG_HOST:
|
||
print(
|
||
f"[区域] keyconfig 主机未命中 host={region_host} "
|
||
f"status={region.status} error={region.error},尝试兼容主机"
|
||
)
|
||
return region
|
||
|
||
|
||
def main() -> int:
|
||
args = build_parser().parse_args()
|
||
|
||
# 设备身份只存在于本次进程;没有加载、种子复现或落盘路径。
|
||
profile = DeviceProfileGenerator().new_profile()
|
||
print(f"[设备] 在线 DFP 注册中(did={profile.did})...")
|
||
try:
|
||
run_online_bootstrap(profile, timeout=REQUEST_TIMEOUT_SECONDS)
|
||
print(f"[设备] 注册完成 did={profile.did} egid={profile.egid}")
|
||
except Exception as exc: # noqa: BLE001
|
||
print(f"[设备] DFP 注册失败,改用本次本地画像继续: {exc}")
|
||
|
||
passport_account_image = generate_profile_passport_account_image(profile)
|
||
print(
|
||
"[passport] 纯 Python 现场生成 "
|
||
f"{passport_account_image[:14]}...{passport_account_image[-12:]} "
|
||
f"(len={len(passport_account_image)})"
|
||
)
|
||
weapon_header_provider = WeaponProofProvider(APK_DEFAULT_WEAPON_KAW)
|
||
print(
|
||
f"[Weapon] KAS 纯 Python 已启用 "
|
||
f"kaw={_mask_secret(APK_DEFAULT_WEAPON_KAW, head=10, tail=8)} "
|
||
f"(len={len(APK_DEFAULT_WEAPON_KAW)}, source=APK z_y_x_a fallback)"
|
||
)
|
||
|
||
sig3_state = _new_sig3_state()
|
||
seed = sig3_state.session_seed
|
||
print(
|
||
f"[sig3] 进程状态 seed=0x{seed:08x} "
|
||
f"counter_start=0x{sig3_state.counter:08x}"
|
||
)
|
||
|
||
base_url = args.base_url.rstrip("/") or None
|
||
login_host = urllib.parse.urlparse(base_url or DEFAULT_LOGIN_HOST).hostname or ""
|
||
http_session = create_http_session(args.transport)
|
||
print(f"[传输] profile={args.transport}")
|
||
|
||
region_ticket = ""
|
||
anonymous_region = _refresh_anonymous_region(profile, http_session)
|
||
if anonymous_region and anonymous_region.ok:
|
||
region_ticket = anonymous_region.ticket
|
||
_set_region_cookie(http_session, region_ticket, host=login_host)
|
||
print(
|
||
f"[区域] keyconfig 匿名票据已刷新 "
|
||
f"uid={anonymous_region.uid or '0'} name={anonymous_region.name or '-'} "
|
||
f"ticket={_mask_secret(region_ticket, head=12, tail=8)}"
|
||
)
|
||
elif anonymous_region:
|
||
print(
|
||
f"[区域] keyconfig 匿名刷新未命中,继续无票据 "
|
||
f"status={anonymous_region.status} error={anonymous_region.error}"
|
||
)
|
||
|
||
code = args.code
|
||
if not code:
|
||
effective_base = base_url or DEFAULT_LOGIN_HOST
|
||
print(
|
||
f"[发码] -> {args.mobile} host={effective_base} "
|
||
f"timeout={REQUEST_TIMEOUT_SECONDS}s type={LOGIN_SMS_CODE_TYPE}"
|
||
)
|
||
send_encrypted_mobile = loginhelper_encrypt_mobile(args.mobile)
|
||
send_captcha_token = ""
|
||
send_retry = 0
|
||
while True:
|
||
response = request_mobile_code(
|
||
profile,
|
||
args.mobile,
|
||
mobile_country_code=LOGIN_COUNTRY_CODE,
|
||
encrypted_mobile=send_encrypted_mobile,
|
||
passport_account_image=passport_account_image,
|
||
code_type=LOGIN_SMS_CODE_TYPE,
|
||
need_check=False,
|
||
prefetch_phone_number="",
|
||
request_source="1",
|
||
captcha_token=send_captcha_token,
|
||
base_url=base_url,
|
||
session_seed=seed,
|
||
sig3_state=sig3_state,
|
||
timeout=REQUEST_TIMEOUT_SECONDS,
|
||
post_func=http_session.post,
|
||
weapon_header_provider=weapon_header_provider,
|
||
)
|
||
body = response.get("body") or {}
|
||
print(
|
||
f" status={response.get('status')} "
|
||
f"body={_json_for_display(body, 300)}"
|
||
)
|
||
if response.get("error"):
|
||
print(f" error={response.get('error')}")
|
||
print(
|
||
"[发码] 请求未拿到响应;如果手机已收到短信,"
|
||
"重新执行并加 --code <验证码> 跳过发码继续登录"
|
||
)
|
||
return 2
|
||
challenge_url = _captcha_error_url({"body": body})
|
||
if body.get("result") == 1 or not (
|
||
challenge_url and send_retry < CAPTCHA_RETRIES
|
||
):
|
||
if body.get("result") != 1:
|
||
print("[发码] 未返回 result=1(可能仍发了码 / 受风控);继续输入收到的验证码")
|
||
break
|
||
send_captcha_token = _complete_captcha_handoff(
|
||
challenge_url,
|
||
stage="requestMobileCode",
|
||
session=http_session,
|
||
)
|
||
if not send_captcha_token:
|
||
return 4
|
||
send_retry += 1
|
||
print(f"[发码] captcha_token 已注入,重放发码 ({send_retry}/{CAPTCHA_RETRIES})")
|
||
code = input("[输入] 收到的短信验证码: ").strip()
|
||
|
||
print(
|
||
f"[登录] mobileVerifyCode mobile={_mask_mobile(args.mobile)} "
|
||
f"code=<CODE> login_type={LOGIN_SMS_CODE_TYPE}"
|
||
)
|
||
login_encrypted_mobile = loginhelper_encrypt_mobile(args.mobile)
|
||
# 验证码重放必须复用同一组账号保护字段,保持原请求正文不变。
|
||
account_security_fields = build_account_security_fields()
|
||
login_captcha_token = ""
|
||
login_retry = 0
|
||
while True:
|
||
login_session, results = login_by_code_try_paths(
|
||
profile,
|
||
args.mobile,
|
||
code,
|
||
mobile_country_code=LOGIN_COUNTRY_CODE,
|
||
encrypted_mobile=login_encrypted_mobile,
|
||
passport_account_image=passport_account_image,
|
||
captcha_token=login_captcha_token,
|
||
prefetch_phone_number=args.mobile,
|
||
account_security_fields=account_security_fields,
|
||
include_prefetch_phone_number=True,
|
||
login_type=LOGIN_SMS_CODE_TYPE,
|
||
base_url=base_url,
|
||
session_seed=seed,
|
||
sig3_state=sig3_state,
|
||
timeout=REQUEST_TIMEOUT_SECONDS,
|
||
post_func=http_session.post,
|
||
weapon_header_provider=weapon_header_provider,
|
||
)
|
||
challenge_url = ""
|
||
for path, candidate_session in results:
|
||
raw = (
|
||
candidate_session.raw
|
||
if isinstance(candidate_session.raw, dict)
|
||
else {}
|
||
)
|
||
print(f"\n[{path}]")
|
||
_print_raw_response(raw)
|
||
_print_login_failure_diagnosis(
|
||
raw, passport_account_image=passport_account_image
|
||
)
|
||
challenge_url = challenge_url or _captcha_error_url(raw)
|
||
if login_session.ok:
|
||
break
|
||
if not (challenge_url and login_retry < CAPTCHA_RETRIES):
|
||
break
|
||
login_captcha_token = _complete_captcha_handoff(
|
||
challenge_url,
|
||
stage="mobileVerifyCode",
|
||
session=http_session,
|
||
)
|
||
if not login_captcha_token:
|
||
return 4
|
||
login_retry += 1
|
||
print(f"[登录] captcha_token 已注入,重放原接口 ({login_retry}/{CAPTCHA_RETRIES})")
|
||
|
||
if not login_session.ok:
|
||
print("\n[FAIL] mobileVerifyCode 未拿到会话(看上面 result/body)")
|
||
return 1
|
||
|
||
http_session.cookies.set("kuaishou.api_st", login_session.api_st)
|
||
http_session.cookies.set("token", login_session.api_st)
|
||
if login_session.h5_st:
|
||
http_session.cookies.set("kuaishou.h5_st", login_session.h5_st)
|
||
if login_session.user_id:
|
||
http_session.cookies.set("userId", login_session.user_id)
|
||
http_session.cookies.set("ud", login_session.user_id)
|
||
|
||
account_region = refresh_region_ticket(
|
||
profile,
|
||
api_st=login_session.api_st,
|
||
client_salt=login_session.client_salt,
|
||
user_id=login_session.user_id,
|
||
base_url=DEFAULT_REGION_HOST,
|
||
timeout=REQUEST_TIMEOUT_SECONDS,
|
||
get_func=http_session.get,
|
||
)
|
||
if account_region.ok:
|
||
region_ticket = account_region.ticket
|
||
_set_region_cookie(http_session, region_ticket, host=login_host)
|
||
print(
|
||
f"[区域] keyconfig 账号票据已刷新 "
|
||
f"uid={account_region.uid or login_session.user_id} "
|
||
f"name={account_region.name or '-'} "
|
||
f"ticket={_mask_secret(region_ticket, head=12, tail=8)}"
|
||
)
|
||
else:
|
||
print(
|
||
f"[区域] keyconfig 账号刷新未命中,保留匿名票据 "
|
||
f"status={account_region.status} error={account_region.error}"
|
||
)
|
||
login_session.region_ticket = region_ticket or login_session.region_ticket
|
||
print("\n[OK] 登录成功,会话:")
|
||
_print_session(login_session)
|
||
_print_account_config(profile, login_session)
|
||
return 0
|
||
|
||
|
||
def entrypoint() -> int:
|
||
try:
|
||
return main()
|
||
except KeyboardInterrupt:
|
||
print("\n[中断] 已停止;如果短信已收到,可用 --code <验证码> 继续登录")
|
||
return 130
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(entrypoint())
|