"""OpenAI Sentinel Token (PoW) 生成与请求工具函数。 用于密码登录、注册等需要 sentinel token 的流程。 """ from __future__ import annotations import base64 import json import random import time import uuid from typing import TYPE_CHECKING if TYPE_CHECKING: from curl_cffi.requests import Session class SentinelTokenGenerator: """Sentinel Token 生成器(PoW - Proof of Work)。""" MAX_ATTEMPTS = 500_000 ERROR_PREFIX = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D" def __init__(self, device_id: str, ua: str): self.device_id = device_id self.user_agent = ua self.sid = str(uuid.uuid4()) @staticmethod def _fnv1a_32(text: str) -> str: h = 2166136261 for ch in text: h ^= ord(ch) h = (h * 16777619) & 0xFFFFFFFF h ^= h >> 16 h = (h * 2246822507) & 0xFFFFFFFF h ^= h >> 13 h = (h * 3266489909) & 0xFFFFFFFF h ^= h >> 16 return format(h & 0xFFFFFFFF, "08x") def _get_config(self) -> list: perf_now = random.uniform(1000, 50000) return [ "1920x1080", time.strftime("%a %b %d %Y %H:%M:%S GMT+0000 (Coordinated Universal Time)", time.gmtime()), 4294705152, random.random(), self.user_agent, "https://sentinel.openai.com/sentinel/20260124ceb8/sdk.js", None, None, "en-US", random.random(), random.choice(["vendorSub-undefined", "plugins-undefined", "mimeTypes-undefined", "hardwareConcurrency-undefined"]), random.choice(["location", "implementation", "URL", "documentURI", "compatMode"]), random.choice(["Object", "Function", "Array", "Number", "parseFloat", "undefined"]), perf_now, self.sid, "", random.choice([4, 8, 12, 16]), time.time() * 1000 - perf_now, ] @staticmethod def _b64(data) -> str: return base64.b64encode(json.dumps(data, separators=(",", ":"), ensure_ascii=False).encode("utf-8")).decode("ascii") def generate_requirements_token(self) -> str: data = self._get_config() data[3] = 1 data[9] = round(random.uniform(5, 50)) return "gAAAAAC" + self._b64(data) def generate_token(self, seed: str, difficulty: str) -> str: start = time.time() data = self._get_config() difficulty = str(difficulty or "0") for i in range(self.MAX_ATTEMPTS): data[3] = i data[9] = round((time.time() - start) * 1000) payload = self._b64(data) if self._fnv1a_32(seed + payload)[: len(difficulty)] <= difficulty: return "gAAAAAB" + payload + "~S" return "gAAAAAB" + self.ERROR_PREFIX + self._b64(str(None)) # ── 默认 User-Agent 和 sec-ch-ua ────────────────────────────── DEFAULT_SENTINEL_USER_AGENT = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/145.0.0.0 Safari/537.36" ) DEFAULT_SENTINEL_SEC_CH_UA = '"Chromium";v="145", "Google Chrome";v="145", "Not/A)Brand";v="99"' def build_sentinel_token( session: "Session", device_id: str, flow: str, *, user_agent: str = "", sec_ch_ua: str = "", ) -> tuple[str, str]: """请求 sentinel token 并返回 (sentinel_header_value, oai_sc_cookie_value)。 Args: session: curl_cffi Session 实例 device_id: 设备 ID flow: 流程标识(如 "password_verify", "username_password_create" 等) user_agent: 可选的 User-Agent 覆盖 sec_ch_ua: 可选的 sec-ch-ua 覆盖 Returns: (openai-sentinel-token header value, oai-sc cookie value) 元组 Raises: RuntimeError: sentinel 请求失败 """ ua = user_agent or DEFAULT_SENTINEL_USER_AGENT ch_ua = sec_ch_ua or DEFAULT_SENTINEL_SEC_CH_UA generator = SentinelTokenGenerator(device_id, ua) resp = session.post( "https://sentinel.openai.com/backend-api/sentinel/req", data=json.dumps({"p": generator.generate_requirements_token(), "id": device_id, "flow": flow}), headers={ "Content-Type": "text/plain;charset=UTF-8", "Referer": "https://sentinel.openai.com/backend-api/sentinel/frame.html", "Origin": "https://sentinel.openai.com", "User-Agent": ua, "sec-ch-ua": ch_ua, "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', }, timeout=20, verify=False, ) try: data = resp.json() if resp.text else {} except Exception: fallback = json.dumps( {"p": generator.generate_requirements_token(), "t": "", "c": "", "id": device_id, "flow": flow}, separators=(",", ":"), ) return fallback, "" token = str(data.get("token") or "").strip() if resp.status_code != 200 or not token: raise RuntimeError(f"sentinel_req_failed_{resp.status_code}") pow_data = data.get("proofofwork") or {} p_value = ( generator.generate_token(str(pow_data.get("seed") or ""), str(pow_data.get("difficulty") or "0")) if pow_data.get("required") and pow_data.get("seed") else generator.generate_requirements_token() ) sentinel_value = json.dumps({"p": p_value, "t": "", "c": token, "id": device_id, "flow": flow}, separators=(",", ":")) # oai-sc cookie = "0" + sentinel token "c" value (the challenge token from the server) oai_sc_value = "0" + token return sentinel_value, oai_sc_value