Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85c270f55f | |||
| 660d43161d | |||
| be8dd745fb | |||
| 2ff52d5d73 | |||
| 2c875594a6 | |||
| 4d5fa36183 | |||
| b9dd421714 | |||
| 4c40949696 | |||
| cb1fb57b53 | |||
| 713564fc25 | |||
| 6b41c9bccd | |||
| 71efc3b04c | |||
| 36bd799c8f | |||
| a7867ae406 | |||
| ae86ca42df | |||
| b50ad199de | |||
| cb55db7901 | |||
| 1c17015669 | |||
| f39dff8ee6 | |||
| 79c3eb733c | |||
| e4f330500d | |||
| 85949d8ede | |||
| fcf1354bc7 | |||
| 6d3aa84af9 | |||
| 11395bf1ba | |||
| ad03fab8e9 | |||
| b7e3cd840b | |||
| 20e2719d0e | |||
| 75a0dccebe | |||
| b7e658c567 | |||
| e43bd390f0 | |||
| eb255fdf77 | |||
| 8d5f8fe3bb | |||
| 52b875a7f9 | |||
| a4f542ace2 | |||
| ad10d1f2b7 | |||
| 06eaff03b9 | |||
| a973343b48 | |||
| c937bc7356 | |||
| e14aabd0e2 | |||
| 935531955f | |||
| 6cafaa4ab7 | |||
| 8cb7a50bb9 | |||
| adb60cdfd6 | |||
| 20cdf8060d | |||
| 6364d43c90 | |||
| 86206f8a97 | |||
| c2aa9785cb | |||
| ccff201fde | |||
| 32e926c4af | |||
| af161cca4f | |||
| 970340fbd4 | |||
| effc1add37 | |||
| 6b914bad41 | |||
| d93383fe23 | |||
| c6ab6b3123 | |||
| fb3ebae995 | |||
| 289e8ec71f | |||
| 0e8b5ba237 | |||
| 7c4688895e |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -33,3 +33,9 @@ Thumbs.db
|
||||
nul
|
||||
|
||||
.claude/settings.local.json
|
||||
autogptplus_drission.py
|
||||
autogptplus_drission_oai.py
|
||||
accounts.json
|
||||
team-reg-go
|
||||
CodexAuth
|
||||
.agent/rules/use.md
|
||||
|
||||
747
api_register.py
Normal file
747
api_register.py
Normal file
@@ -0,0 +1,747 @@
|
||||
"""
|
||||
ChatGPT API 注册模块 (协议模式)
|
||||
- 使用 curl_cffi 通过 API 快速完成注册
|
||||
- 支持 Cookie 注入到浏览器完成支付
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
import re
|
||||
import uuid
|
||||
from urllib.parse import unquote
|
||||
|
||||
try:
|
||||
from curl_cffi import requests as curl_requests
|
||||
CURL_CFFI_AVAILABLE = True
|
||||
except ImportError:
|
||||
CURL_CFFI_AVAILABLE = False
|
||||
curl_requests = None
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def _is_shutdown_requested():
|
||||
"""检查是否收到停止请求"""
|
||||
try:
|
||||
import run
|
||||
return run._shutdown_requested
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class ShutdownRequested(Exception):
|
||||
"""用户请求停止异常"""
|
||||
pass
|
||||
|
||||
|
||||
def log_status(step, message):
|
||||
"""日志输出"""
|
||||
timestamp = time.strftime("%H:%M:%S")
|
||||
print(f"[{timestamp}] [{step}] {message}")
|
||||
|
||||
|
||||
def log_progress(message):
|
||||
"""进度输出"""
|
||||
print(f" {message}")
|
||||
|
||||
|
||||
def request_with_retry(func, *args, max_retries=3, **kwargs):
|
||||
"""带重试的请求"""
|
||||
for i in range(max_retries):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
if i == max_retries - 1:
|
||||
raise e
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
class ChatGPTAPIRegister:
|
||||
"""ChatGPT API 注册类 (协议模式)"""
|
||||
|
||||
def __init__(self, proxy=None):
|
||||
"""初始化
|
||||
|
||||
Args:
|
||||
proxy: 代理地址,如 "http://127.0.0.1:7890"
|
||||
"""
|
||||
if not CURL_CFFI_AVAILABLE:
|
||||
raise ImportError("协议模式需要安装 curl_cffi: pip install curl_cffi")
|
||||
|
||||
self.session = curl_requests.Session(
|
||||
impersonate="edge",
|
||||
verify=False,
|
||||
proxies={"http": proxy, "https": proxy} if proxy else {}
|
||||
)
|
||||
self.auth_session_logging_id = str(uuid.uuid4())
|
||||
self.oai_did = ""
|
||||
self.csrf_token = ""
|
||||
self.authorize_url = ""
|
||||
self.access_token = ""
|
||||
|
||||
def init_session(self) -> bool:
|
||||
"""初始化会话,获取必要的 cookies 和 tokens"""
|
||||
try:
|
||||
resp = request_with_retry(self.session.get, "https://chatgpt.com")
|
||||
if resp.status_code != 200:
|
||||
log_progress(f"[X] 初始化失败: HTTP {resp.status_code}")
|
||||
return False
|
||||
|
||||
self.oai_did = self.session.cookies.get("oai-did")
|
||||
csrf_cookie = self.session.cookies.get("__Host-next-auth.csrf-token")
|
||||
|
||||
if csrf_cookie:
|
||||
self.csrf_token = unquote(csrf_cookie).split("|")[0]
|
||||
else:
|
||||
log_progress("[X] 未获取到 CSRF token")
|
||||
return False
|
||||
|
||||
# 访问登录页面
|
||||
request_with_retry(
|
||||
self.session.get,
|
||||
f"https://chatgpt.com/auth/login?openaicom-did={self.oai_did}"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
log_progress(f"[X] 初始化异常: {e}")
|
||||
return False
|
||||
|
||||
def get_authorize_url(self, email: str) -> bool:
|
||||
"""获取授权 URL"""
|
||||
try:
|
||||
url = f"https://chatgpt.com/api/auth/signin/openai?prompt=login&ext-oai-did={self.oai_did}&auth_session_logging_id={self.auth_session_logging_id}&screen_hint=login_or_signup&login_hint={email}"
|
||||
payload = {
|
||||
"callbackUrl": "https://chatgpt.com/",
|
||||
"csrfToken": self.csrf_token,
|
||||
"json": "true"
|
||||
}
|
||||
resp = request_with_retry(
|
||||
self.session.post, url, data=payload,
|
||||
headers={"Origin": "https://chatgpt.com"}
|
||||
)
|
||||
data = resp.json()
|
||||
|
||||
if data.get("url") and "auth.openai.com" in data["url"]:
|
||||
self.authorize_url = data["url"]
|
||||
return True
|
||||
|
||||
log_progress(f"[X] 授权 URL 无效: {data}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_progress(f"[X] 获取授权 URL 异常: {e}")
|
||||
return False
|
||||
|
||||
def start_authorize(self) -> bool:
|
||||
"""启动授权流程"""
|
||||
try:
|
||||
resp = request_with_retry(
|
||||
self.session.get, self.authorize_url, allow_redirects=True
|
||||
)
|
||||
return "create-account" in resp.url or "log-in" in resp.url
|
||||
except Exception as e:
|
||||
log_progress(f"[X] 启动授权异常: {e}")
|
||||
return False
|
||||
|
||||
def register(self, email: str, password: str) -> bool:
|
||||
"""注册账户"""
|
||||
try:
|
||||
resp = request_with_retry(
|
||||
self.session.post,
|
||||
"https://auth.openai.com/api/accounts/user/register",
|
||||
json={"password": password, "username": email},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Origin": "https://auth.openai.com"
|
||||
}
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
return True
|
||||
log_progress(f"[X] 注册失败: {resp.status_code} - {resp.text[:200]}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_progress(f"[X] 注册异常: {e}")
|
||||
return False
|
||||
|
||||
def send_verification_email(self) -> bool:
|
||||
"""发送验证邮件"""
|
||||
try:
|
||||
resp = request_with_retry(
|
||||
self.session.get,
|
||||
"https://auth.openai.com/api/accounts/email-otp/send",
|
||||
allow_redirects=True
|
||||
)
|
||||
return resp.status_code == 200
|
||||
except Exception as e:
|
||||
log_progress(f"[X] 发送验证邮件异常: {e}")
|
||||
return False
|
||||
|
||||
def validate_otp(self, otp_code: str) -> bool:
|
||||
"""验证 OTP 码"""
|
||||
try:
|
||||
resp = request_with_retry(
|
||||
self.session.post,
|
||||
"https://auth.openai.com/api/accounts/email-otp/validate",
|
||||
json={"code": otp_code},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Origin": "https://auth.openai.com"
|
||||
}
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
return True
|
||||
log_progress(f"[X] OTP 验证失败: {resp.status_code} - {resp.text[:200]}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_progress(f"[X] OTP 验证异常: {e}")
|
||||
return False
|
||||
|
||||
def create_account(self, name: str, birthdate: str) -> bool:
|
||||
"""创建账户 (填写姓名和生日)
|
||||
|
||||
Args:
|
||||
name: 姓名
|
||||
birthdate: 生日,格式 "YYYY-MM-DD"
|
||||
"""
|
||||
try:
|
||||
resp = request_with_retry(
|
||||
self.session.post,
|
||||
"https://auth.openai.com/api/accounts/create_account",
|
||||
json={"name": name, "birthdate": birthdate},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Origin": "https://auth.openai.com"
|
||||
}
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
log_progress(f"[X] 创建账户失败: {resp.status_code} - {resp.text[:200]}")
|
||||
return False
|
||||
|
||||
# 检查响应是否为空
|
||||
resp_text = resp.text.strip()
|
||||
if not resp_text:
|
||||
log_progress("[X] 创建账户失败: 服务器返回空响应")
|
||||
return False
|
||||
|
||||
try:
|
||||
data = resp.json()
|
||||
except Exception as json_err:
|
||||
log_progress(f"[X] 创建账户失败: JSON 解析错误 - {json_err}")
|
||||
return False
|
||||
|
||||
continue_url = data.get("continue_url")
|
||||
if continue_url:
|
||||
request_with_retry(self.session.get, continue_url, allow_redirects=True)
|
||||
return True
|
||||
except Exception as e:
|
||||
log_progress(f"[X] 创建账户异常: {e}")
|
||||
return False
|
||||
|
||||
def login(self, email: str, password: str) -> bool:
|
||||
"""使用密码登录 (复用注册时建立的会话)"""
|
||||
try:
|
||||
resp = request_with_retry(
|
||||
self.session.post,
|
||||
"https://auth.openai.com/api/accounts/password/verify",
|
||||
json={"username": email, "password": password},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Origin": "https://auth.openai.com"
|
||||
}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
continue_url = data.get("continue_url")
|
||||
if continue_url:
|
||||
request_with_retry(self.session.get, continue_url, allow_redirects=True)
|
||||
return True
|
||||
|
||||
log_progress(f"[X] 登录失败: {resp.status_code} - {resp.text[:200]}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_progress(f"[X] 登录异常: {e}")
|
||||
return False
|
||||
|
||||
def get_session_token(self) -> str:
|
||||
"""获取 access token"""
|
||||
try:
|
||||
resp = request_with_retry(self.session.get, "https://chatgpt.com/api/auth/session")
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
token = data.get("accessToken")
|
||||
if token:
|
||||
self.access_token = token
|
||||
return token
|
||||
log_progress(f"[X] Session 响应无 token: {str(data)[:200]}")
|
||||
else:
|
||||
log_progress(f"[X] Session 请求失败: {resp.status_code}")
|
||||
return ""
|
||||
except Exception as e:
|
||||
log_progress(f"[X] 获取 token 异常: {e}")
|
||||
return ""
|
||||
|
||||
def get_checkout_url(self) -> str:
|
||||
"""通过 API 获取支付页 URL"""
|
||||
try:
|
||||
token = self.access_token or self.get_session_token()
|
||||
if not token:
|
||||
log_progress("[X] 无法获取 access token")
|
||||
return ""
|
||||
|
||||
payload = {
|
||||
"plan_name": "chatgptteamplan",
|
||||
"team_plan_data": {
|
||||
"workspace_name": "Sepa",
|
||||
"price_interval": "month",
|
||||
"seat_quantity": 5
|
||||
},
|
||||
"billing_details": {
|
||||
"country": "DE",
|
||||
"currency": "EUR"
|
||||
},
|
||||
"promo_campaign": {
|
||||
"promo_campaign_id": "team-1-month-free",
|
||||
"is_coupon_from_query_param": True
|
||||
},
|
||||
"checkout_ui_mode": "redirect"
|
||||
}
|
||||
|
||||
resp = request_with_retry(
|
||||
self.session.post,
|
||||
"https://chatgpt.com/backend-api/payments/checkout",
|
||||
json=payload,
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
"Origin": "https://chatgpt.com"
|
||||
}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
checkout_url = data.get("url")
|
||||
if checkout_url:
|
||||
return checkout_url
|
||||
log_progress(f"[X] 响应无 URL: {resp.text[:200]}")
|
||||
else:
|
||||
log_progress(f"[X] 获取支付页失败: {resp.status_code} - {resp.text[:200]}")
|
||||
return ""
|
||||
except Exception as e:
|
||||
log_progress(f"[X] 获取支付页异常: {e}")
|
||||
return ""
|
||||
|
||||
def get_cookies(self) -> list:
|
||||
"""获取所有 cookies 用于注入浏览器"""
|
||||
cookies = []
|
||||
for cookie in self.session.cookies.jar:
|
||||
cookies.append({
|
||||
"name": cookie.name,
|
||||
"value": cookie.value,
|
||||
"domain": cookie.domain,
|
||||
"path": cookie.path or "/",
|
||||
"secure": cookie.secure,
|
||||
})
|
||||
return cookies
|
||||
|
||||
|
||||
def get_verification_code_api(target_email: str, mail_api_base: str, mail_api_token: str, max_retries: int = 90) -> str:
|
||||
"""通过 API 获取验证码
|
||||
|
||||
Args:
|
||||
target_email: 目标邮箱
|
||||
mail_api_base: 邮件 API 地址
|
||||
mail_api_token: 邮件 API Token
|
||||
max_retries: 最大重试次数
|
||||
|
||||
Returns:
|
||||
str: 验证码,失败返回空字符串
|
||||
|
||||
Raises:
|
||||
ShutdownRequested: 用户请求停止时抛出
|
||||
"""
|
||||
log_status("API监听", "正在监听邮件...")
|
||||
headers = {"Authorization": mail_api_token, "Content-Type": "application/json"}
|
||||
start_time = time.time()
|
||||
|
||||
for i in range(max_retries):
|
||||
# 检查停止请求
|
||||
if _is_shutdown_requested():
|
||||
log_status("停止", "[!] 检测到停止请求,中断邮件监听")
|
||||
raise ShutdownRequested("用户请求停止")
|
||||
|
||||
elapsed = int(time.time() - start_time)
|
||||
try:
|
||||
url = f"{mail_api_base}/api/public/emailList"
|
||||
payload = {"toEmail": target_email, "timeSort": "desc", "size": 20}
|
||||
resp = requests.post(url, headers=headers, json=payload, timeout=10)
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
if data.get('code') == 200:
|
||||
mails = data.get('data', [])
|
||||
if mails:
|
||||
for mail in mails:
|
||||
html_body = mail.get('content') or mail.get('text') or str(mail)
|
||||
code_match = re.search(r'\b(\d{6})\b', html_body)
|
||||
if code_match:
|
||||
code = code_match.group(1)
|
||||
# 如果是已知的无效验证码,跳过继续等待新的
|
||||
if code == "783500":
|
||||
log_status("跳过", f"[!] 检测到旧验证码 {code},继续等待新验证码...")
|
||||
continue
|
||||
log_status("捕获", f"[OK] 提取到验证码: {code}")
|
||||
return code
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if i % 5 == 0:
|
||||
print(f" [监听中] 已耗时 {elapsed}秒...")
|
||||
time.sleep(2)
|
||||
|
||||
log_status("超时", "[X] 未能获取验证码")
|
||||
return ""
|
||||
|
||||
|
||||
def api_register_flow(
|
||||
email: str,
|
||||
password: str,
|
||||
real_name: str,
|
||||
birthdate: str,
|
||||
mail_api_base: str,
|
||||
mail_api_token: str,
|
||||
proxy: str = None,
|
||||
progress_callback=None
|
||||
) -> ChatGPTAPIRegister:
|
||||
"""执行 API 注册流程
|
||||
|
||||
Args:
|
||||
email: 邮箱
|
||||
password: 密码
|
||||
real_name: 姓名
|
||||
birthdate: 生日 (YYYY-MM-DD)
|
||||
mail_api_base: 邮件 API 地址
|
||||
mail_api_token: 邮件 API Token
|
||||
proxy: 代理地址
|
||||
progress_callback: 进度回调
|
||||
|
||||
Returns:
|
||||
ChatGPTAPIRegister: 成功返回 reg 对象,失败返回 None
|
||||
|
||||
Raises:
|
||||
ShutdownRequested: 用户请求停止时抛出
|
||||
"""
|
||||
def log_cb(msg):
|
||||
if progress_callback:
|
||||
progress_callback(msg)
|
||||
else:
|
||||
log_progress(msg)
|
||||
|
||||
def check_shutdown():
|
||||
"""检查停止请求"""
|
||||
if _is_shutdown_requested():
|
||||
log_cb("[!] 检测到停止请求")
|
||||
raise ShutdownRequested("用户请求停止")
|
||||
|
||||
reg = ChatGPTAPIRegister(proxy=proxy)
|
||||
|
||||
try:
|
||||
check_shutdown()
|
||||
log_status("API注册", "初始化会话...")
|
||||
if not reg.init_session():
|
||||
log_cb("[X] 初始化失败")
|
||||
return None
|
||||
log_cb("[OK] 会话初始化成功")
|
||||
|
||||
check_shutdown()
|
||||
log_status("API注册", "获取授权 URL...")
|
||||
if not reg.get_authorize_url(email):
|
||||
log_cb("[X] 获取授权 URL 失败")
|
||||
return None
|
||||
log_cb("[OK] 授权 URL 获取成功")
|
||||
|
||||
check_shutdown()
|
||||
log_status("API注册", "开始授权流程...")
|
||||
if not reg.start_authorize():
|
||||
log_cb("[X] 授权流程启动失败")
|
||||
return None
|
||||
log_cb("[OK] 授权流程已启动")
|
||||
|
||||
check_shutdown()
|
||||
log_status("API注册", "注册账户...")
|
||||
if not reg.register(email, password):
|
||||
log_cb("[X] 注册失败")
|
||||
return None
|
||||
log_cb("[OK] 账户注册成功")
|
||||
|
||||
check_shutdown()
|
||||
log_status("API注册", "发送验证邮件...")
|
||||
if not reg.send_verification_email():
|
||||
log_cb("[X] 发送验证邮件失败")
|
||||
return None
|
||||
log_cb("[OK] 验证邮件已发送")
|
||||
|
||||
check_shutdown()
|
||||
# 获取验证码
|
||||
otp_code = get_verification_code_api(email, mail_api_base, mail_api_token)
|
||||
if not otp_code:
|
||||
log_cb("[X] 未能获取验证码")
|
||||
return None
|
||||
|
||||
check_shutdown()
|
||||
log_status("API注册", f"验证 OTP: {otp_code}")
|
||||
if not reg.validate_otp(otp_code):
|
||||
log_cb("[X] OTP 验证失败")
|
||||
return None
|
||||
log_cb("[OK] OTP 验证成功")
|
||||
|
||||
check_shutdown()
|
||||
# 创建账户(带重试)
|
||||
log_status("API注册", "创建账户...")
|
||||
create_success = reg.create_account(real_name, birthdate)
|
||||
|
||||
# 如果创建失败,重新获取验证码再试一次
|
||||
if not create_success:
|
||||
check_shutdown()
|
||||
log_cb("[!] 创建账户失败,尝试重新验证...")
|
||||
|
||||
# 重新发送验证邮件
|
||||
log_status("API注册", "重新发送验证邮件...")
|
||||
if not reg.send_verification_email():
|
||||
log_cb("[X] 重新发送验证邮件失败")
|
||||
return None
|
||||
log_cb("[OK] 验证邮件已重新发送")
|
||||
|
||||
check_shutdown()
|
||||
# 重新获取验证码
|
||||
time.sleep(2) # 等待新邮件
|
||||
otp_code = get_verification_code_api(email, mail_api_base, mail_api_token)
|
||||
if not otp_code:
|
||||
log_cb("[X] 未能获取新验证码")
|
||||
return None
|
||||
|
||||
check_shutdown()
|
||||
log_status("API注册", f"重新验证 OTP: {otp_code}")
|
||||
if not reg.validate_otp(otp_code):
|
||||
log_cb("[X] OTP 重新验证失败")
|
||||
return None
|
||||
log_cb("[OK] OTP 重新验证成功")
|
||||
|
||||
check_shutdown()
|
||||
# 再次尝试创建账户
|
||||
log_status("API注册", "重新创建账户...")
|
||||
if not reg.create_account(real_name, birthdate):
|
||||
log_cb("[X] 创建账户仍然失败")
|
||||
return None
|
||||
|
||||
log_cb("[OK] 账户创建成功")
|
||||
|
||||
check_shutdown()
|
||||
# 验证 session 是否有效
|
||||
token = reg.get_session_token()
|
||||
if token:
|
||||
log_cb(f"[OK] Session 有效,Token: {token[:30]}...")
|
||||
else:
|
||||
log_cb("[!] 注册完成但 session 可能未完全建立")
|
||||
|
||||
return reg
|
||||
|
||||
except ShutdownRequested:
|
||||
raise # 重新抛出停止请求异常
|
||||
except Exception as e:
|
||||
log_status("错误", f"注册异常: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def api_login_flow(
|
||||
email: str,
|
||||
password: str,
|
||||
proxy: str = None,
|
||||
progress_callback=None
|
||||
) -> ChatGPTAPIRegister:
|
||||
"""执行 API 登录流程
|
||||
|
||||
Args:
|
||||
email: 邮箱
|
||||
password: 密码
|
||||
proxy: 代理地址
|
||||
progress_callback: 进度回调
|
||||
|
||||
Returns:
|
||||
ChatGPTAPIRegister: 成功返回 reg 对象,失败返回 None
|
||||
"""
|
||||
def log_cb(msg):
|
||||
if progress_callback:
|
||||
progress_callback(msg)
|
||||
else:
|
||||
log_progress(msg)
|
||||
|
||||
reg = ChatGPTAPIRegister(proxy=proxy)
|
||||
|
||||
try:
|
||||
log_status("API登录", "初始化会话...")
|
||||
if not reg.init_session():
|
||||
log_cb("[X] 初始化失败")
|
||||
return None
|
||||
log_cb("[OK] 初始化成功")
|
||||
|
||||
log_status("API登录", "获取授权 URL...")
|
||||
if not reg.get_authorize_url(email):
|
||||
log_cb("[X] 获取授权 URL 失败")
|
||||
return None
|
||||
log_cb("[OK] 获取授权 URL 成功")
|
||||
|
||||
log_status("API登录", "开始授权流程...")
|
||||
if not reg.start_authorize():
|
||||
log_cb("[X] 授权流程失败")
|
||||
return None
|
||||
log_cb("[OK] 授权流程成功")
|
||||
|
||||
log_status("API登录", "密码验证...")
|
||||
if not reg.login(email, password):
|
||||
log_cb("[X] 登录失败")
|
||||
return None
|
||||
log_cb("[OK] 登录成功")
|
||||
|
||||
# 获取 token
|
||||
token = reg.get_session_token()
|
||||
if token:
|
||||
log_status("API登录", f"Token: {token[:50]}...")
|
||||
|
||||
return reg
|
||||
|
||||
except Exception as e:
|
||||
log_status("错误", f"登录异常: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def is_api_mode_available() -> bool:
|
||||
"""检查协议模式是否可用"""
|
||||
return CURL_CFFI_AVAILABLE
|
||||
|
||||
|
||||
def api_register_account_only(
|
||||
email: str,
|
||||
password: str,
|
||||
real_name: str,
|
||||
birthdate: str,
|
||||
get_verification_code_func,
|
||||
proxy: str = None,
|
||||
progress_callback=None
|
||||
) -> bool:
|
||||
"""仅执行 API 注册流程(不含支付,用于邀请邮箱注册)
|
||||
|
||||
Args:
|
||||
email: 邮箱
|
||||
password: 密码
|
||||
real_name: 姓名
|
||||
birthdate: 生日 (YYYY-MM-DD)
|
||||
get_verification_code_func: 获取验证码的函数,签名: func(email) -> str
|
||||
proxy: 代理地址
|
||||
progress_callback: 进度回调
|
||||
|
||||
Returns:
|
||||
bool: 是否注册成功
|
||||
"""
|
||||
def log_cb(msg):
|
||||
if progress_callback:
|
||||
progress_callback(msg)
|
||||
else:
|
||||
log_progress(msg)
|
||||
|
||||
if not CURL_CFFI_AVAILABLE:
|
||||
log_status("错误", "协议模式不可用,请安装 curl_cffi")
|
||||
return False
|
||||
|
||||
reg = ChatGPTAPIRegister(proxy=proxy)
|
||||
|
||||
try:
|
||||
log_status("API注册", "初始化会话...")
|
||||
if not reg.init_session():
|
||||
log_cb("[X] 初始化失败")
|
||||
return False
|
||||
log_cb("[OK] 会话初始化成功")
|
||||
|
||||
log_status("API注册", "获取授权 URL...")
|
||||
if not reg.get_authorize_url(email):
|
||||
log_cb("[X] 获取授权 URL 失败")
|
||||
return False
|
||||
log_cb("[OK] 授权 URL 获取成功")
|
||||
|
||||
log_status("API注册", "开始授权流程...")
|
||||
if not reg.start_authorize():
|
||||
log_cb("[X] 授权流程启动失败")
|
||||
return False
|
||||
log_cb("[OK] 授权流程已启动")
|
||||
|
||||
log_status("API注册", "注册账户...")
|
||||
if not reg.register(email, password):
|
||||
log_cb("[X] 注册失败")
|
||||
return False
|
||||
log_cb("[OK] 账户注册成功")
|
||||
|
||||
log_status("API注册", "发送验证邮件...")
|
||||
if not reg.send_verification_email():
|
||||
log_cb("[X] 发送验证邮件失败")
|
||||
return False
|
||||
log_cb("[OK] 验证邮件已发送")
|
||||
|
||||
# 使用传入的函数获取验证码
|
||||
log_status("API注册", "等待验证码...")
|
||||
otp_code = get_verification_code_func(email)
|
||||
if not otp_code:
|
||||
log_cb("[X] 未能获取验证码")
|
||||
return False
|
||||
|
||||
log_status("API注册", f"验证 OTP: {otp_code}")
|
||||
if not reg.validate_otp(otp_code):
|
||||
log_cb("[X] OTP 验证失败")
|
||||
return False
|
||||
log_cb("[OK] OTP 验证成功")
|
||||
|
||||
log_status("API注册", "创建账户...")
|
||||
create_success = reg.create_account(real_name, birthdate)
|
||||
|
||||
# 如果创建失败,重新获取验证码再试一次
|
||||
if not create_success:
|
||||
log_cb("[!] 创建账户失败,尝试重新验证...")
|
||||
|
||||
# 重新发送验证邮件
|
||||
log_status("API注册", "重新发送验证邮件...")
|
||||
if not reg.send_verification_email():
|
||||
log_cb("[X] 重新发送验证邮件失败")
|
||||
return False
|
||||
log_cb("[OK] 验证邮件已重新发送")
|
||||
|
||||
# 重新获取验证码
|
||||
time.sleep(2) # 等待新邮件
|
||||
otp_code = get_verification_code_func(email)
|
||||
if not otp_code:
|
||||
log_cb("[X] 未能获取新验证码")
|
||||
return False
|
||||
|
||||
log_status("API注册", f"重新验证 OTP: {otp_code}")
|
||||
if not reg.validate_otp(otp_code):
|
||||
log_cb("[X] OTP 重新验证失败")
|
||||
return False
|
||||
log_cb("[OK] OTP 重新验证成功")
|
||||
|
||||
# 再次尝试创建账户
|
||||
log_status("API注册", "重新创建账户...")
|
||||
if not reg.create_account(real_name, birthdate):
|
||||
log_cb("[X] 创建账户仍然失败")
|
||||
return False
|
||||
|
||||
log_cb("[OK] 账户创建成功")
|
||||
|
||||
# 验证 session 是否有效
|
||||
token = reg.get_session_token()
|
||||
if token:
|
||||
log_cb(f"[OK] 注册完成,Token: {token[:30]}...")
|
||||
return True
|
||||
else:
|
||||
log_cb("[!] 注册完成但 session 可能未完全建立")
|
||||
return True # 仍然返回成功,因为注册流程已完成
|
||||
|
||||
except Exception as e:
|
||||
log_status("错误", f"注册异常: {e}")
|
||||
return False
|
||||
2496
auto_gpt_team.py
Normal file
2496
auto_gpt_team.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
63
config.py
63
config.py
@@ -307,11 +307,16 @@ def reload_config() -> dict:
|
||||
"""
|
||||
global _cfg, _raw_teams, TEAMS
|
||||
global EMAIL_PROVIDER, INCLUDE_TEAM_OWNERS, AUTH_PROVIDER
|
||||
global EMAIL_API_BASE, EMAIL_API_AUTH, EMAIL_DOMAINS, EMAIL_DOMAIN
|
||||
global BROWSER_HEADLESS, ACCOUNTS_PER_TEAM
|
||||
global GPTMAIL_API_KEYS, GPTMAIL_DOMAINS, GPTMAIL_PREFIX
|
||||
global PROXY_ENABLED, PROXIES
|
||||
global S2A_API_BASE, S2A_ADMIN_KEY, S2A_ADMIN_TOKEN
|
||||
global S2A_CONCURRENCY, S2A_PRIORITY, S2A_GROUP_NAMES, S2A_GROUP_IDS
|
||||
global S2A_CONCURRENCY, S2A_PRIORITY, S2A_GROUP_NAMES, S2A_GROUP_IDS, S2A_API_MODE
|
||||
global CONCURRENT_ENABLED, CONCURRENT_WORKERS
|
||||
global SCHEDULER_ENABLED, SCHEDULER_START_HOUR, SCHEDULER_END_HOUR
|
||||
global SCHEDULER_BATCH_SIZE, SCHEDULER_COOLDOWN_MINUTES, SCHEDULER_OUTPUT_TYPE
|
||||
global SCHEDULER_MAX_CONSECUTIVE_FAILURES
|
||||
|
||||
result = {
|
||||
"success": True,
|
||||
@@ -349,12 +354,24 @@ def reload_config() -> dict:
|
||||
_account = _cfg.get("account", {})
|
||||
ACCOUNTS_PER_TEAM = _account.get("accounts_per_team", 4)
|
||||
|
||||
# 并发配置
|
||||
_concurrent = _cfg.get("concurrent", {})
|
||||
CONCURRENT_ENABLED = _concurrent.get("enabled", False)
|
||||
CONCURRENT_WORKERS = _concurrent.get("workers", 4)
|
||||
|
||||
# GPTMail 配置
|
||||
_gptmail = _cfg.get("gptmail", {})
|
||||
GPTMAIL_PREFIX = _gptmail.get("prefix", "")
|
||||
GPTMAIL_DOMAINS = _gptmail.get("domains", [])
|
||||
GPTMAIL_API_KEYS = _gptmail.get("api_keys", []) or ["gpt-test"]
|
||||
|
||||
# Cloud Mail (email) 配置
|
||||
_email = _cfg.get("email", {})
|
||||
EMAIL_API_BASE = _email.get("api_base", "")
|
||||
EMAIL_API_AUTH = _email.get("api_auth", "")
|
||||
EMAIL_DOMAINS = _email.get("domains", []) or ([_email["domain"]] if _email.get("domain") else [])
|
||||
EMAIL_DOMAIN = EMAIL_DOMAINS[0] if EMAIL_DOMAINS else ""
|
||||
|
||||
# 代理配置
|
||||
_proxy_enabled_top = _cfg.get("proxy_enabled")
|
||||
_proxy_enabled_browser = _cfg.get("browser", {}).get("proxy_enabled")
|
||||
@@ -373,6 +390,17 @@ def reload_config() -> dict:
|
||||
S2A_PRIORITY = _s2a.get("priority", 50)
|
||||
S2A_GROUP_NAMES = _s2a.get("group_names", [])
|
||||
S2A_GROUP_IDS = _s2a.get("group_ids", [])
|
||||
S2A_API_MODE = _s2a.get("api_mode", False)
|
||||
|
||||
# 定时调度器配置
|
||||
_scheduler = _cfg.get("scheduler", {})
|
||||
SCHEDULER_ENABLED = _scheduler.get("enabled", False)
|
||||
SCHEDULER_START_HOUR = _scheduler.get("start_hour", 8)
|
||||
SCHEDULER_END_HOUR = _scheduler.get("end_hour", 14)
|
||||
SCHEDULER_BATCH_SIZE = _scheduler.get("batch_size", 50)
|
||||
SCHEDULER_COOLDOWN_MINUTES = _scheduler.get("cooldown_minutes", 5)
|
||||
SCHEDULER_OUTPUT_TYPE = _scheduler.get("output_type", "team")
|
||||
SCHEDULER_MAX_CONSECUTIVE_FAILURES = _scheduler.get("max_consecutive_failures", 3)
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"config.toml: {e}")
|
||||
@@ -404,7 +432,7 @@ def reload_config() -> dict:
|
||||
# 邮箱系统选择
|
||||
EMAIL_PROVIDER = _cfg.get("email_provider", "kyx") # "kyx" 或 "gptmail"
|
||||
|
||||
# 原有邮箱系统 (KYX)
|
||||
# 原有邮箱系统 (KYX / Cloud Mail)
|
||||
_email = _cfg.get("email", {})
|
||||
EMAIL_API_BASE = _email.get("api_base", "")
|
||||
EMAIL_API_AUTH = _email.get("api_auth", "")
|
||||
@@ -413,6 +441,17 @@ EMAIL_DOMAIN = EMAIL_DOMAINS[0] if EMAIL_DOMAINS else ""
|
||||
EMAIL_ROLE = _email.get("role", "gpt-team")
|
||||
EMAIL_WEB_URL = _email.get("web_url", "")
|
||||
|
||||
|
||||
def get_cloudmail_api_base() -> str:
|
||||
"""获取 Cloud Mail API 地址,自动补全 /api/public 路径"""
|
||||
if not EMAIL_API_BASE:
|
||||
return ""
|
||||
api_base = EMAIL_API_BASE.rstrip("/")
|
||||
if not api_base.endswith("/api/public"):
|
||||
api_base = f"{api_base}/api/public"
|
||||
return api_base
|
||||
|
||||
|
||||
# GPTMail 临时邮箱配置
|
||||
_gptmail = _cfg.get("gptmail", {})
|
||||
GPTMAIL_API_BASE = _gptmail.get("api_base", "https://mail.chatgpt.org.uk")
|
||||
@@ -594,12 +633,18 @@ S2A_CONCURRENCY = _s2a.get("concurrency", 10)
|
||||
S2A_PRIORITY = _s2a.get("priority", 50)
|
||||
S2A_GROUP_NAMES = _s2a.get("group_names", [])
|
||||
S2A_GROUP_IDS = _s2a.get("group_ids", [])
|
||||
S2A_API_MODE = _s2a.get("api_mode", False) # 是否使用纯 API 授权模式 (无需浏览器)
|
||||
|
||||
# 账号
|
||||
_account = _cfg.get("account", {})
|
||||
DEFAULT_PASSWORD = _account.get("default_password", "kfcvivo50")
|
||||
ACCOUNTS_PER_TEAM = _account.get("accounts_per_team", 4)
|
||||
|
||||
# 并发处理配置
|
||||
_concurrent = _cfg.get("concurrent", {})
|
||||
CONCURRENT_ENABLED = _concurrent.get("enabled", False) # 是否启用并发处理
|
||||
CONCURRENT_WORKERS = _concurrent.get("workers", 4) # 并发数量 (浏览器实例数)
|
||||
|
||||
# 注册
|
||||
_reg = _cfg.get("register", {})
|
||||
REGISTER_NAME = _reg.get("name", "test")
|
||||
@@ -645,6 +690,16 @@ TELEGRAM_NOTIFY_ON_ERROR = _telegram.get("notify_on_error", True)
|
||||
TELEGRAM_CHECK_INTERVAL = _telegram.get("check_interval", 3600) # 默认1小时检查一次
|
||||
TELEGRAM_LOW_STOCK_THRESHOLD = _telegram.get("low_stock_threshold", 10) # 低库存阈值
|
||||
|
||||
# 定时调度器配置
|
||||
_scheduler = _cfg.get("scheduler", {})
|
||||
SCHEDULER_ENABLED = _scheduler.get("enabled", False) # 是否启用定时调度
|
||||
SCHEDULER_START_HOUR = _scheduler.get("start_hour", 8) # 开始时间 (小时, 0-23)
|
||||
SCHEDULER_END_HOUR = _scheduler.get("end_hour", 14) # 结束时间 (小时, 0-23)
|
||||
SCHEDULER_BATCH_SIZE = _scheduler.get("batch_size", 50) # 每轮注册数量
|
||||
SCHEDULER_COOLDOWN_MINUTES = _scheduler.get("cooldown_minutes", 5) # 轮次间冷却 (分钟)
|
||||
SCHEDULER_OUTPUT_TYPE = _scheduler.get("output_type", "team") # 输出方式: team / json
|
||||
SCHEDULER_MAX_CONSECUTIVE_FAILURES = _scheduler.get("max_consecutive_failures", 3) # 连续失败N轮后暂停
|
||||
|
||||
# 代理
|
||||
# 注意: proxy_enabled 和 proxies 可能在顶层或被误放在 browser section 下
|
||||
_proxy_enabled_top = _cfg.get("proxy_enabled")
|
||||
@@ -925,12 +980,12 @@ def get_random_domain() -> str:
|
||||
|
||||
def generate_random_email(prefix_len: int = 8) -> str:
|
||||
prefix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=prefix_len))
|
||||
return f"{prefix}oaiteam@{get_random_domain()}"
|
||||
return f"team-{prefix}oaiteam@{get_random_domain()}"
|
||||
|
||||
|
||||
def generate_email_for_user(username: str) -> str:
|
||||
safe = re.sub(r'[^a-zA-Z0-9]', '', username.lower())[:20]
|
||||
return f"{safe}oaiteam@{get_random_domain()}"
|
||||
return f"team-{safe}oaiteam@{get_random_domain()}"
|
||||
|
||||
|
||||
def get_team(index: int = 0) -> dict:
|
||||
|
||||
@@ -154,6 +154,11 @@ priority = 50
|
||||
group_ids = []
|
||||
# 分组名称列表 (优先使用 group_ids,如果未配置则通过名称查询 ID)
|
||||
group_names = []
|
||||
# 是否使用纯 API 授权模式 (无需浏览器)
|
||||
# 开启后使用 curl_cffi 直接调用 OpenAI 认证 API 完成授权
|
||||
# 优点: 更快、更稳定、无需浏览器
|
||||
# 缺点: 需要安装 curl_cffi (pip install curl_cffi)
|
||||
api_mode = false
|
||||
|
||||
# ==================== 账号配置 ====================
|
||||
[account]
|
||||
@@ -162,6 +167,15 @@ default_password = "YourSecurePassword@2025"
|
||||
# 每个 Team 下创建的账号数量
|
||||
accounts_per_team = 4
|
||||
|
||||
# ==================== 并发处理配置 ====================
|
||||
# 启用后可同时处理多个账号,大幅提升效率
|
||||
[concurrent]
|
||||
# 是否启用并发处理 (默认关闭)
|
||||
enabled = false
|
||||
# 并发数量 (同时运行的浏览器实例数)
|
||||
# 建议根据机器配置设置: 4核8G内存建议设置为 2-4
|
||||
workers = 4
|
||||
|
||||
# ==================== 注册配置 ====================
|
||||
[register]
|
||||
# 注册时使用的用户名 (实际会使用随机生成的英文名)
|
||||
@@ -220,3 +234,44 @@ notify_on_error = true
|
||||
check_interval = 3600
|
||||
# 低库存预警阈值 (正常账号数低于此值时预警)
|
||||
low_stock_threshold = 10
|
||||
|
||||
# ==================== 定时调度器配置 ====================
|
||||
# 时间窗口内自动循环执行: 注册 → run_all → 冷却 → 重复
|
||||
# 通过 Telegram Bot 的 /schedule 命令开启/关闭
|
||||
[scheduler]
|
||||
# 是否启用定时调度 (也可通过 /schedule on 命令开启)
|
||||
enabled = false
|
||||
# 时间窗口: 仅在此时间段内运行 (24小时制)
|
||||
start_hour = 8
|
||||
end_hour = 14
|
||||
# 每轮注册的 GPT Team 账号数量
|
||||
batch_size = 50
|
||||
# 每轮完成后的冷却时间 (分钟)
|
||||
cooldown_minutes = 5
|
||||
# 注册输出方式: "team" (写入 team.json 供 run_all 处理)
|
||||
output_type = "team"
|
||||
# 连续失败 N 轮后自动暂停调度器并发送告警
|
||||
max_consecutive_failures = 3
|
||||
|
||||
# ==================== AutoGPTPlus 配置 ====================
|
||||
# 独立的 ChatGPT 订阅自动化脚本配置
|
||||
[autogptplus]
|
||||
# Cloud Mail API Token
|
||||
mail_api_token = "your-cloud-mail-token"
|
||||
# Cloud Mail API 地址
|
||||
mail_api_base = "https://your-cloud-mail.com"
|
||||
# 可用邮箱域名列表
|
||||
email_domains = ["@example.com", "@example.org"]
|
||||
# SEPA IBAN 列表 (也可通过 Bot /iban_add 命令导入到 sepa_ibans.txt)
|
||||
sepa_ibans = []
|
||||
# 是否启用随机指纹 (User-Agent, WebGL, 分辨率等)
|
||||
random_fingerprint = true
|
||||
|
||||
# 注册模式选择:
|
||||
# - "api": 协议模式 (默认),使用 API 快速完成注册,仅支付环节使用浏览器
|
||||
# 协议模式更快,需要安装 curl_cffi: pip install curl_cffi
|
||||
# - "browser": 浏览器自动化模式,全程使用 DrissionPage 浏览器自动化
|
||||
register_mode = "api"
|
||||
|
||||
# 协议模式代理 (仅协议模式使用,格式: http://127.0.0.1:7890)
|
||||
api_proxy = ""
|
||||
|
||||
@@ -27,6 +27,7 @@ from config import (
|
||||
get_random_gptmail_domain,
|
||||
get_next_gptmail_key,
|
||||
get_gptmail_keys,
|
||||
get_cloudmail_api_base,
|
||||
)
|
||||
from logger import log
|
||||
|
||||
@@ -425,7 +426,7 @@ def generate_random_email() -> str:
|
||||
"""生成随机邮箱地址: {random_str}oaiteam@{random_domain}"""
|
||||
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
||||
domain = get_random_domain()
|
||||
email = f"{random_str}oaiteam@{domain}"
|
||||
email = f"team-{random_str}oaiteam@{domain}"
|
||||
log.success(f"生成邮箱: {email}")
|
||||
return email
|
||||
|
||||
@@ -441,14 +442,17 @@ def create_email_user(email: str, password: str = None, role_name: str = None) -
|
||||
Returns:
|
||||
tuple: (success, message)
|
||||
"""
|
||||
# 每次调用时重新获取配置,支持动态切换
|
||||
from config import EMAIL_API_AUTH as current_auth, EMAIL_ROLE as current_role
|
||||
|
||||
if password is None:
|
||||
password = DEFAULT_PASSWORD
|
||||
if role_name is None:
|
||||
role_name = EMAIL_ROLE
|
||||
role_name = current_role
|
||||
|
||||
url = f"{EMAIL_API_BASE}/addUser"
|
||||
url = f"{get_cloudmail_api_base()}/addUser"
|
||||
headers = {
|
||||
"Authorization": EMAIL_API_AUTH,
|
||||
"Authorization": current_auth,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
payload = {
|
||||
@@ -484,9 +488,12 @@ def get_verification_code(email: str, max_retries: int = None, interval: int = N
|
||||
Returns:
|
||||
tuple: (code, error, email_time) - 验证码、错误信息、邮件时间
|
||||
"""
|
||||
url = f"{EMAIL_API_BASE}/emailList"
|
||||
# 每次调用时重新获取配置,支持动态切换
|
||||
from config import EMAIL_API_AUTH as current_auth
|
||||
|
||||
url = f"{get_cloudmail_api_base()}/emailList"
|
||||
headers = {
|
||||
"Authorization": EMAIL_API_AUTH,
|
||||
"Authorization": current_auth,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
payload = {"toEmail": email}
|
||||
@@ -565,9 +572,12 @@ def fetch_email_content(email: str) -> list:
|
||||
Returns:
|
||||
list: 邮件列表
|
||||
"""
|
||||
url = f"{EMAIL_API_BASE}/emailList"
|
||||
# 每次调用时重新获取配置,支持动态切换
|
||||
from config import EMAIL_API_AUTH as current_auth
|
||||
|
||||
url = f"{get_cloudmail_api_base()}/emailList"
|
||||
headers = {
|
||||
"Authorization": EMAIL_API_AUTH,
|
||||
"Authorization": current_auth,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
payload = {"toEmail": email}
|
||||
@@ -631,17 +641,20 @@ def unified_generate_email() -> str:
|
||||
Returns:
|
||||
str: 邮箱地址
|
||||
"""
|
||||
if EMAIL_PROVIDER == "gptmail":
|
||||
# 每次调用时重新获取配置,支持动态切换
|
||||
from config import EMAIL_PROVIDER as current_provider
|
||||
|
||||
if current_provider == "gptmail":
|
||||
# 生成随机前缀 + oaiteam 后缀,确保不重复
|
||||
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
||||
prefix = f"{random_str}-oaiteam"
|
||||
prefix = f"team-{random_str}-oaiteam"
|
||||
domain = get_random_gptmail_domain() or None
|
||||
email, error = gptmail_service.generate_email(prefix=prefix, domain=domain)
|
||||
if email:
|
||||
return email
|
||||
log.warning(f"GPTMail 生成失败,回退到 KYX: {error}")
|
||||
|
||||
# 默认使用 KYX 系统
|
||||
# 默认使用 KYX / Cloud Mail 系统
|
||||
return generate_random_email()
|
||||
|
||||
|
||||
@@ -651,10 +664,13 @@ def unified_create_email() -> tuple[str, str]:
|
||||
Returns:
|
||||
tuple: (email, password)
|
||||
"""
|
||||
if EMAIL_PROVIDER == "gptmail":
|
||||
# 每次调用时重新获取配置,支持动态切换
|
||||
from config import EMAIL_PROVIDER as current_provider
|
||||
|
||||
if current_provider == "gptmail":
|
||||
# 生成随机前缀 + oaiteam 后缀,确保不重复
|
||||
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
||||
prefix = f"{random_str}-oaiteam"
|
||||
prefix = f"team-{random_str}-oaiteam"
|
||||
domain = get_random_gptmail_domain() or None
|
||||
email, error = gptmail_service.generate_email(prefix=prefix, domain=domain)
|
||||
if email:
|
||||
@@ -662,7 +678,7 @@ def unified_create_email() -> tuple[str, str]:
|
||||
return email, DEFAULT_PASSWORD
|
||||
log.warning(f"GPTMail 生成失败,回退到 KYX: {error}")
|
||||
|
||||
# 默认使用 KYX 系统
|
||||
# 默认使用 KYX / Cloud Mail 系统
|
||||
email = generate_random_email()
|
||||
success, msg = create_email_user(email, DEFAULT_PASSWORD)
|
||||
if success or "已存在" in msg:
|
||||
@@ -681,10 +697,13 @@ def unified_get_verification_code(email: str, max_retries: int = None, interval:
|
||||
Returns:
|
||||
tuple: (code, error, email_time) - 验证码、错误信息、邮件时间
|
||||
"""
|
||||
if EMAIL_PROVIDER == "gptmail":
|
||||
# 每次调用时重新获取配置,支持动态切换
|
||||
from config import EMAIL_PROVIDER as current_provider
|
||||
|
||||
if current_provider == "gptmail":
|
||||
return gptmail_service.get_verification_code(email, max_retries, interval)
|
||||
|
||||
# 默认使用 KYX 系统
|
||||
# 默认使用 KYX / Cloud Mail 系统
|
||||
return get_verification_code(email, max_retries, interval)
|
||||
|
||||
|
||||
@@ -697,9 +716,12 @@ def unified_fetch_emails(email: str) -> list:
|
||||
Returns:
|
||||
list: 邮件列表
|
||||
"""
|
||||
if EMAIL_PROVIDER == "gptmail":
|
||||
# 每次调用时重新获取配置,支持动态切换
|
||||
from config import EMAIL_PROVIDER as current_provider
|
||||
|
||||
if current_provider == "gptmail":
|
||||
emails, error = gptmail_service.get_emails(email)
|
||||
return emails
|
||||
|
||||
# 默认使用 KYX 系统
|
||||
# 默认使用 KYX / Cloud Mail 系统
|
||||
return fetch_email_content(email)
|
||||
|
||||
354
proxy_pool.py
Normal file
354
proxy_pool.py
Normal file
@@ -0,0 +1,354 @@
|
||||
"""
|
||||
代理池管理模块
|
||||
- 从 proxy.txt 加载代理
|
||||
- 并发测试代理可用性
|
||||
- 线程安全的轮询分配
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
import concurrent.futures
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# 尝试导入 curl_cffi (更好的指纹伪装)
|
||||
try:
|
||||
from curl_cffi import requests as curl_requests
|
||||
CURL_AVAILABLE = True
|
||||
except ImportError:
|
||||
curl_requests = None
|
||||
CURL_AVAILABLE = False
|
||||
|
||||
import requests
|
||||
|
||||
log = logging.getLogger("proxy_pool")
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
PROXY_FILE = BASE_DIR / "proxy.txt"
|
||||
|
||||
# 测试目标 URL
|
||||
TEST_URL = "https://api.openai.com/v1/models"
|
||||
TEST_TIMEOUT = 10 # 秒
|
||||
|
||||
|
||||
def _mask_proxy(proxy_url: str) -> str:
|
||||
"""脱敏代理 URL (隐藏用户名密码)"""
|
||||
if "@" in proxy_url:
|
||||
parts = proxy_url.split("@")
|
||||
scheme_auth = parts[0]
|
||||
host_part = parts[-1]
|
||||
if "://" in scheme_auth:
|
||||
scheme = scheme_auth.split("://")[0]
|
||||
return f"{scheme}://***@{host_part}"
|
||||
return f"***@{host_part}"
|
||||
return proxy_url
|
||||
|
||||
|
||||
def parse_proxy_url(proxy_url: str) -> dict | None:
|
||||
"""解析代理 URL,返回结构化信息
|
||||
|
||||
支持格式:
|
||||
http://host:port
|
||||
http://username:password@host:port
|
||||
socks5://host:port
|
||||
socks5://username:password@host:port
|
||||
|
||||
Returns:
|
||||
dict: {"url": str, "scheme": str, "host": str, "port": int, "username": str, "password": str}
|
||||
None: 格式无效
|
||||
"""
|
||||
proxy_url = proxy_url.strip()
|
||||
if not proxy_url:
|
||||
return None
|
||||
|
||||
# 确保有 scheme
|
||||
if not proxy_url.startswith(("http://", "https://", "socks5://", "socks4://")):
|
||||
proxy_url = "http://" + proxy_url
|
||||
|
||||
try:
|
||||
parsed = urlparse(proxy_url)
|
||||
if not parsed.hostname or not parsed.port:
|
||||
return None
|
||||
|
||||
return {
|
||||
"url": proxy_url,
|
||||
"scheme": parsed.scheme,
|
||||
"host": parsed.hostname,
|
||||
"port": parsed.port,
|
||||
"username": parsed.username or "",
|
||||
"password": parsed.password or "",
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def load_proxies() -> list[str]:
|
||||
"""从 proxy.txt 加载代理列表
|
||||
|
||||
Returns:
|
||||
list[str]: 代理 URL 列表
|
||||
"""
|
||||
if not PROXY_FILE.exists():
|
||||
log.info("[ProxyPool] proxy.txt 不存在")
|
||||
return []
|
||||
|
||||
proxies = []
|
||||
invalid_count = 0
|
||||
try:
|
||||
with open(PROXY_FILE, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
parsed = parse_proxy_url(line)
|
||||
if parsed:
|
||||
proxies.append(parsed["url"])
|
||||
else:
|
||||
invalid_count += 1
|
||||
log.warning(f"[ProxyPool] 格式无效,已跳过: {line}")
|
||||
except Exception as e:
|
||||
log.error(f"[ProxyPool] 读取 proxy.txt 失败: {e}")
|
||||
|
||||
log.info(f"[ProxyPool] 从 proxy.txt 加载 {len(proxies)} 个代理" + (f",{invalid_count} 个格式无效" if invalid_count else ""))
|
||||
return proxies
|
||||
|
||||
|
||||
def save_proxies(proxies: list[str]):
|
||||
"""保存代理列表到 proxy.txt (保留文件头部注释)
|
||||
|
||||
Args:
|
||||
proxies: 代理 URL 列表
|
||||
"""
|
||||
header_lines = []
|
||||
|
||||
if PROXY_FILE.exists():
|
||||
try:
|
||||
with open(PROXY_FILE, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if line.strip().startswith("#") or not line.strip():
|
||||
header_lines.append(line.rstrip())
|
||||
else:
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
with open(PROXY_FILE, "w", encoding="utf-8") as f:
|
||||
if header_lines:
|
||||
f.write("\n".join(header_lines) + "\n")
|
||||
for proxy in proxies:
|
||||
f.write(proxy + "\n")
|
||||
log.info(f"[ProxyPool] 已保存 {len(proxies)} 个代理到 proxy.txt")
|
||||
except Exception as e:
|
||||
log.error(f"[ProxyPool] 保存代理文件失败: {e}")
|
||||
|
||||
|
||||
def test_single_proxy(proxy_url: str, timeout: int = TEST_TIMEOUT) -> dict:
|
||||
"""测试单个代理是否可用
|
||||
|
||||
Args:
|
||||
proxy_url: 代理 URL
|
||||
timeout: 超时秒数
|
||||
|
||||
Returns:
|
||||
dict: {"proxy": str, "alive": bool, "latency_ms": int, "error": str}
|
||||
"""
|
||||
proxies_dict = {"http": proxy_url, "https": proxy_url}
|
||||
masked = _mask_proxy(proxy_url)
|
||||
start = time.time()
|
||||
|
||||
try:
|
||||
if CURL_AVAILABLE:
|
||||
resp = curl_requests.head(
|
||||
TEST_URL,
|
||||
proxies=proxies_dict,
|
||||
timeout=timeout,
|
||||
verify=False,
|
||||
impersonate="edge",
|
||||
)
|
||||
else:
|
||||
resp = requests.head(
|
||||
TEST_URL,
|
||||
proxies=proxies_dict,
|
||||
timeout=timeout,
|
||||
verify=False,
|
||||
)
|
||||
latency = int((time.time() - start) * 1000)
|
||||
log.info(f"[ProxyPool] ✅ {masked} - {latency}ms")
|
||||
return {"proxy": proxy_url, "alive": True, "latency_ms": latency, "error": ""}
|
||||
except Exception as e:
|
||||
latency = int((time.time() - start) * 1000)
|
||||
err_msg = str(e)[:80]
|
||||
log.info(f"[ProxyPool] ❌ {masked} - 失败 ({err_msg})")
|
||||
return {"proxy": proxy_url, "alive": False, "latency_ms": latency, "error": err_msg}
|
||||
|
||||
|
||||
class ProxyPool:
|
||||
"""线程安全的代理池"""
|
||||
|
||||
def __init__(self):
|
||||
self._working_proxies: list[str] = []
|
||||
self._index = 0
|
||||
self._lock = threading.Lock()
|
||||
self._last_test_time: float = 0
|
||||
self._last_test_results: dict = {} # {total, alive, removed}
|
||||
|
||||
def reload(self) -> int:
|
||||
"""从文件重新加载代理
|
||||
|
||||
Returns:
|
||||
int: 加载的代理数量
|
||||
"""
|
||||
with self._lock:
|
||||
self._working_proxies = load_proxies()
|
||||
self._index = 0
|
||||
count = len(self._working_proxies)
|
||||
log.info(f"[ProxyPool] 代理池已重新加载,共 {count} 个代理")
|
||||
return count
|
||||
|
||||
def test_and_clean(self, concurrency: int = 20, timeout: int = TEST_TIMEOUT) -> dict:
|
||||
"""并发测试所有代理,移除不可用的
|
||||
|
||||
Args:
|
||||
concurrency: 并发数
|
||||
timeout: 单个代理超时秒数
|
||||
|
||||
Returns:
|
||||
dict: {"total": int, "alive": int, "removed": int, "duration": float, "details": list}
|
||||
"""
|
||||
# 先从文件加载最新
|
||||
all_proxies = load_proxies()
|
||||
if not all_proxies:
|
||||
log.info("[ProxyPool] 代理池为空,跳过测试")
|
||||
self._last_test_results = {"total": 0, "alive": 0, "removed": 0, "duration": 0, "details": []}
|
||||
return self._last_test_results
|
||||
|
||||
total = len(all_proxies)
|
||||
start_time = time.time()
|
||||
log.info(f"[ProxyPool] ========== 开始测试 {total} 个代理 (并发: {concurrency}) ==========")
|
||||
|
||||
# 并发测试
|
||||
alive_proxies = []
|
||||
details = [] # 每个代理的详细结果
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
|
||||
future_to_proxy = {
|
||||
executor.submit(test_single_proxy, proxy, timeout): proxy
|
||||
for proxy in all_proxies
|
||||
}
|
||||
|
||||
for future in concurrent.futures.as_completed(future_to_proxy):
|
||||
proxy = future_to_proxy[future]
|
||||
try:
|
||||
result = future.result()
|
||||
details.append(result)
|
||||
if result["alive"]:
|
||||
alive_proxies.append(proxy)
|
||||
except Exception as e:
|
||||
details.append({"proxy": proxy, "alive": False, "latency_ms": 0, "error": str(e)[:50]})
|
||||
|
||||
# 按原始顺序排序 details
|
||||
proxy_order = {p: i for i, p in enumerate(all_proxies)}
|
||||
details.sort(key=lambda d: proxy_order.get(d["proxy"], 999))
|
||||
|
||||
duration = time.time() - start_time
|
||||
|
||||
# 更新工作代理池 (保持原始顺序)
|
||||
ordered_alive = [p for p in all_proxies if p in set(alive_proxies)]
|
||||
with self._lock:
|
||||
self._working_proxies = ordered_alive
|
||||
self._index = 0
|
||||
|
||||
# 保存存活的代理到文件
|
||||
dead_count = total - len(ordered_alive)
|
||||
if dead_count > 0:
|
||||
save_proxies(ordered_alive)
|
||||
|
||||
# 统计延迟
|
||||
alive_latencies = [d["latency_ms"] for d in details if d["alive"]]
|
||||
avg_ms = int(sum(alive_latencies) / len(alive_latencies)) if alive_latencies else 0
|
||||
|
||||
log.info(
|
||||
f"[ProxyPool] ========== 测试完成 =========="
|
||||
f" | 总计: {total} | 存活: {len(ordered_alive)} | 移除: {dead_count}"
|
||||
f" | 平均延迟: {avg_ms}ms | 耗时: {round(duration, 1)}s"
|
||||
)
|
||||
|
||||
self._last_test_time = time.time()
|
||||
self._last_test_results = {
|
||||
"total": total,
|
||||
"alive": len(ordered_alive),
|
||||
"removed": dead_count,
|
||||
"duration": round(duration, 1),
|
||||
"details": details,
|
||||
}
|
||||
|
||||
return self._last_test_results
|
||||
|
||||
def get_next_proxy(self) -> str | None:
|
||||
"""获取下一个代理 (轮询)
|
||||
|
||||
Returns:
|
||||
str: 代理 URL,池为空时返回 None
|
||||
"""
|
||||
with self._lock:
|
||||
if not self._working_proxies:
|
||||
return None
|
||||
proxy = self._working_proxies[self._index % len(self._working_proxies)]
|
||||
self._index += 1
|
||||
return proxy
|
||||
|
||||
def get_proxy_count(self) -> int:
|
||||
"""获取当前可用代理数量"""
|
||||
with self._lock:
|
||||
return len(self._working_proxies)
|
||||
|
||||
def get_status(self) -> dict:
|
||||
"""获取代理池状态
|
||||
|
||||
Returns:
|
||||
dict: {"count": int, "last_test_time": float, "last_test_results": dict}
|
||||
"""
|
||||
with self._lock:
|
||||
return {
|
||||
"count": len(self._working_proxies),
|
||||
"proxies": list(self._working_proxies),
|
||||
"last_test_time": self._last_test_time,
|
||||
"last_test_results": self._last_test_results,
|
||||
}
|
||||
|
||||
|
||||
# ============ 全局单例 ============
|
||||
_pool = ProxyPool()
|
||||
|
||||
|
||||
def get_pool() -> ProxyPool:
|
||||
"""获取全局代理池实例"""
|
||||
return _pool
|
||||
|
||||
|
||||
def reload_proxies() -> int:
|
||||
"""重新加载代理"""
|
||||
return _pool.reload()
|
||||
|
||||
|
||||
def test_and_clean_proxies(concurrency: int = 20) -> dict:
|
||||
"""并发测试并清理代理"""
|
||||
return _pool.test_and_clean(concurrency=concurrency)
|
||||
|
||||
|
||||
def get_next_proxy() -> str | None:
|
||||
"""获取下一个代理 (轮询)"""
|
||||
return _pool.get_next_proxy()
|
||||
|
||||
|
||||
def get_proxy_count() -> int:
|
||||
"""获取可用代理数量"""
|
||||
return _pool.get_proxy_count()
|
||||
|
||||
|
||||
def get_proxy_status() -> dict:
|
||||
"""获取代理池状态"""
|
||||
return _pool.get_status()
|
||||
@@ -5,6 +5,7 @@ description = "OpenAI Team 账号自动批量注册 & CRS 入库工具"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"curl-cffi>=0.14.0",
|
||||
"drissionpage>=4.1.1.2",
|
||||
"python-telegram-bot[job-queue]>=22.5",
|
||||
"requests>=2.32.5",
|
||||
|
||||
@@ -4,3 +4,6 @@ requests>=2.32.5
|
||||
rich>=14.2.0
|
||||
setuptools>=80.9.0
|
||||
tomli>=2.3.0
|
||||
|
||||
# 协议模式依赖 (可选,用于 API 快速注册)
|
||||
curl_cffi>=0.7.0
|
||||
|
||||
772
run.py
772
run.py
@@ -5,9 +5,9 @@
|
||||
# 1. 检查未完成账号 (自动恢复)
|
||||
# 2. 批量创建邮箱 (4个)
|
||||
# 3. 一次性邀请到 Team
|
||||
# 4. 逐个注册 OpenAI 账号
|
||||
# 4. 逐个注册 OpenAI 账号 (或并发处理)
|
||||
# 5. 逐个 Codex 授权
|
||||
# 6. 逐个添加到 CRS
|
||||
# 6. 逐个添加到 CRS/S2A
|
||||
# 7. 切换下一个 Team
|
||||
|
||||
import time
|
||||
@@ -15,18 +15,21 @@ import random
|
||||
import signal
|
||||
import sys
|
||||
import atexit
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
from config import (
|
||||
TEAMS, ACCOUNTS_PER_TEAM, DEFAULT_PASSWORD, AUTH_PROVIDER,
|
||||
add_domain_to_blacklist, get_domain_from_email, is_email_blacklisted,
|
||||
save_team_json, get_next_proxy
|
||||
save_team_json, get_next_proxy,
|
||||
CONCURRENT_ENABLED, CONCURRENT_WORKERS
|
||||
)
|
||||
from email_service import batch_create_emails, unified_create_email
|
||||
from team_service import batch_invite_to_team, print_team_summary, check_available_seats, invite_single_to_team, preload_all_account_ids
|
||||
from crs_service import crs_add_account, crs_sync_team_owners, crs_verify_token
|
||||
from cpa_service import cpa_verify_connection
|
||||
from s2a_service import s2a_verify_connection
|
||||
from browser_automation import register_and_authorize, login_and_authorize_with_otp, authorize_only, login_and_authorize_team_owner, ShutdownRequested
|
||||
from browser_automation import register_and_authorize, login_and_authorize_with_otp, authorize_only, login_and_authorize_team_owner, ShutdownRequested, register_only
|
||||
from utils import (
|
||||
save_to_csv,
|
||||
load_team_tracker,
|
||||
@@ -58,6 +61,8 @@ except ImportError:
|
||||
_tracker = None
|
||||
_current_results = []
|
||||
_shutdown_requested = False
|
||||
_tracker_lock = threading.Lock() # 用于并发时保护 tracker 操作
|
||||
_auth_callback_lock = threading.Lock() # 授权回调锁 - 确保同一时间只有一个线程进行授权回调
|
||||
|
||||
|
||||
def _save_state():
|
||||
@@ -128,7 +133,7 @@ def process_single_team(team: dict, team_index: int = 0, teams_total: int = 0) -
|
||||
# 如果普通成员已完成目标数量,且没有未完成的 Owner,跳过
|
||||
owner_incomplete = len(owner_accounts)
|
||||
if member_count >= ACCOUNTS_PER_TEAM and completed_count == member_count and owner_incomplete == 0:
|
||||
print_team_summary(team)
|
||||
# 已完成的 Team 直接跳过,不调用 API
|
||||
log.success(f"{team_name} 已完成 {completed_count}/{ACCOUNTS_PER_TEAM} 个成员账号,跳过")
|
||||
return results, []
|
||||
|
||||
@@ -224,12 +229,21 @@ def process_single_team(team: dict, team_index: int = 0, teams_total: int = 0) -
|
||||
|
||||
# ========== 阶段 3: 处理所有账号 (注册 + Codex 授权 + 入库) ==========
|
||||
if all_to_process:
|
||||
log.section(f"阶段 3: 逐个注册 OpenAI + Codex 授权 + 入库")
|
||||
all_results = process_accounts(
|
||||
all_to_process, team_name,
|
||||
team_index=team_index, teams_total=teams_total,
|
||||
include_owner=include_owner
|
||||
)
|
||||
# 根据配置选择处理模式
|
||||
if CONCURRENT_ENABLED and len(all_to_process) > 1:
|
||||
log.section(f"阶段 3: 并发处理 {len(all_to_process)} 个账号 (并发数: {min(CONCURRENT_WORKERS, len(all_to_process))})")
|
||||
all_results = process_accounts_concurrent(
|
||||
all_to_process, team_name,
|
||||
team_index=team_index, teams_total=teams_total,
|
||||
include_owner=include_owner
|
||||
)
|
||||
else:
|
||||
log.section(f"阶段 3: 逐个注册 OpenAI + Codex 授权 + 入库")
|
||||
all_results = process_accounts(
|
||||
all_to_process, team_name,
|
||||
team_index=team_index, teams_total=teams_total,
|
||||
include_owner=include_owner
|
||||
)
|
||||
results.extend(all_results)
|
||||
|
||||
# ========== Team 处理完成 ==========
|
||||
@@ -393,7 +407,26 @@ def process_accounts(accounts: list, team_name: str, team_index: int = 0,
|
||||
else:
|
||||
# 新账号: 注册 + Codex 授权
|
||||
progress_update(phase="注册", step="注册 OpenAI...")
|
||||
register_success, codex_data = register_and_authorize(email, password)
|
||||
register_success, codex_data, new_email_info = register_and_authorize(email, password, team_name=team_name)
|
||||
|
||||
# 如果使用了新邮箱,更新 tracker
|
||||
if new_email_info:
|
||||
new_email = new_email_info["email"]
|
||||
new_password = new_email_info["password"]
|
||||
log.info(f"验证码超时,已切换到新邮箱: {new_email}")
|
||||
|
||||
# 从 tracker 中移除旧邮箱
|
||||
remove_account_from_tracker(_tracker, team_name, email)
|
||||
|
||||
# 添加新邮箱到 tracker
|
||||
add_account_with_password(_tracker, team_name, new_email, new_password, "registered")
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
# 更新当前处理的邮箱信息
|
||||
email = new_email
|
||||
password = new_password
|
||||
result["email"] = email
|
||||
result["password"] = password
|
||||
|
||||
# 检查是否是域名黑名单错误
|
||||
if register_success == "domain_blacklisted":
|
||||
@@ -421,6 +454,29 @@ def process_accounts(accounts: list, team_name: str, team_index: int = 0,
|
||||
|
||||
continue # 跳过当前账号,继续下一个
|
||||
|
||||
# 检查是否需要重新生成邮箱重试 (API 模式失败)
|
||||
if register_success == "retry_new_email":
|
||||
log.warning("API 注册失败,重新生成邮箱重试...")
|
||||
|
||||
# 从 tracker 中移除旧邮箱
|
||||
remove_account_from_tracker(_tracker, team_name, email)
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
# 创建新邮箱
|
||||
new_email, new_password = unified_create_email()
|
||||
if new_email and not is_email_blacklisted(new_email):
|
||||
# 邀请新邮箱
|
||||
if invite_single_to_team(new_email, _get_team_by_name(team_name)):
|
||||
add_account_with_password(_tracker, team_name, new_email, new_password, "invited")
|
||||
save_team_tracker(_tracker)
|
||||
log.success(f"已创建新邮箱: {new_email},将在下次运行时处理")
|
||||
else:
|
||||
log.error("新邮箱邀请失败")
|
||||
else:
|
||||
log.error("无法创建有效的新邮箱")
|
||||
|
||||
continue # 跳过当前账号,继续下一个
|
||||
|
||||
if register_success and register_success != "domain_blacklisted":
|
||||
update_account_status(_tracker, team_name, email, "registered")
|
||||
save_team_tracker(_tracker)
|
||||
@@ -528,20 +584,578 @@ def process_accounts(accounts: list, team_name: str, team_index: int = 0,
|
||||
|
||||
# 账号之间的间隔
|
||||
if i < len(accounts) - 1 and not _shutdown_requested:
|
||||
wait_time = random.randint(3, 6)
|
||||
wait_time = 1
|
||||
log.info(f"等待 {wait_time}s 后处理下一个账号...", icon="wait")
|
||||
time.sleep(wait_time)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# ==================== 并发处理函数 ====================
|
||||
|
||||
def _process_single_account_worker(
|
||||
account: dict,
|
||||
team_name: str,
|
||||
worker_id: int
|
||||
) -> dict:
|
||||
"""单个账号处理工作函数 (用于并发执行)
|
||||
|
||||
Args:
|
||||
account: 账号信息 {"email", "password", "status", "role"}
|
||||
team_name: Team 名称
|
||||
worker_id: 工作线程 ID
|
||||
|
||||
Returns:
|
||||
dict: 处理结果
|
||||
"""
|
||||
global _tracker, _shutdown_requested
|
||||
|
||||
email = account["email"]
|
||||
password = account["password"]
|
||||
role = account.get("role", "member")
|
||||
account_status = account.get("status", "")
|
||||
account_role = account.get("role", "member")
|
||||
|
||||
result = {
|
||||
"team": team_name,
|
||||
"email": email,
|
||||
"password": password,
|
||||
"status": "failed",
|
||||
"crs_id": "",
|
||||
"worker_id": worker_id
|
||||
}
|
||||
|
||||
# 检查中断请求
|
||||
if _shutdown_requested:
|
||||
log.warning(f"[Worker-{worker_id}] 检测到中断请求,跳过: {email}")
|
||||
return result
|
||||
|
||||
# 检查邮箱域名黑名单
|
||||
if is_email_blacklisted(email):
|
||||
domain = get_domain_from_email(email)
|
||||
log.warning(f"[Worker-{worker_id}] 邮箱域名 {domain} 在黑名单中,跳过: {email}")
|
||||
with _tracker_lock:
|
||||
remove_account_from_tracker(_tracker, team_name, email)
|
||||
save_team_tracker(_tracker)
|
||||
return result
|
||||
|
||||
# 已完成的账号跳过
|
||||
if account_status == "completed":
|
||||
log.info(f"[Worker-{worker_id}] 账号已完成,跳过: {email}")
|
||||
result["status"] = "completed"
|
||||
return result
|
||||
|
||||
log.info(f"[Worker-{worker_id}] 开始处理: {email}", icon="account")
|
||||
|
||||
# 判断处理流程
|
||||
is_team_owner_otp = account_status == "team_owner"
|
||||
|
||||
if AUTH_PROVIDER == "s2a":
|
||||
need_crs_only = account_status == "authorized"
|
||||
else:
|
||||
need_crs_only = account_status in ["authorized", "partial"]
|
||||
|
||||
need_auth_only = (
|
||||
account_status in ["registered", "auth_failed"]
|
||||
or (AUTH_PROVIDER == "s2a" and account_status == "partial")
|
||||
or (account_role == "owner" and account_status not in ["team_owner", "completed", "authorized", "partial"])
|
||||
)
|
||||
|
||||
# 标记为处理中
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "processing")
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
try:
|
||||
with Timer(f"[Worker-{worker_id}] 账号 {email}"):
|
||||
if is_team_owner_otp:
|
||||
log.info(f"[Worker-{worker_id}] Team Owner (OTP 登录)...", icon="auth")
|
||||
auth_success, codex_data = login_and_authorize_with_otp(email)
|
||||
register_success = auth_success
|
||||
elif need_crs_only:
|
||||
log.info(f"[Worker-{worker_id}] 已授权账号,跳过授权...", icon="auth")
|
||||
register_success = True
|
||||
codex_data = None
|
||||
if AUTH_PROVIDER not in ("cpa", "s2a"):
|
||||
auth_success, codex_data = authorize_only(email, password)
|
||||
register_success = auth_success
|
||||
elif need_auth_only:
|
||||
log.info(f"[Worker-{worker_id}] 已注册账号,密码登录授权...", icon="auth")
|
||||
auth_success, codex_data = authorize_only(email, password)
|
||||
register_success = True
|
||||
else:
|
||||
log.info(f"[Worker-{worker_id}] 新账号,注册 + 授权...", icon="auth")
|
||||
register_success, codex_data, new_email_info = register_and_authorize(email, password, team_name=team_name)
|
||||
|
||||
# 如果使用了新邮箱,更新 tracker
|
||||
if new_email_info:
|
||||
new_email = new_email_info["email"]
|
||||
new_password = new_email_info["password"]
|
||||
log.info(f"[Worker-{worker_id}] 验证码超时,已切换到新邮箱: {new_email}")
|
||||
|
||||
with _tracker_lock:
|
||||
# 从 tracker 中移除旧邮箱
|
||||
remove_account_from_tracker(_tracker, team_name, email)
|
||||
# 添加新邮箱到 tracker
|
||||
add_account_with_password(_tracker, team_name, new_email, new_password, "registered")
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
# 更新当前处理的邮箱信息
|
||||
email = new_email
|
||||
password = new_password
|
||||
result["email"] = email
|
||||
result["password"] = password
|
||||
|
||||
if register_success == "domain_blacklisted":
|
||||
domain = get_domain_from_email(email)
|
||||
log.error(f"[Worker-{worker_id}] 域名 {domain} 不被支持")
|
||||
add_domain_to_blacklist(domain)
|
||||
with _tracker_lock:
|
||||
remove_account_from_tracker(_tracker, team_name, email)
|
||||
save_team_tracker(_tracker)
|
||||
return result
|
||||
|
||||
# 检查是否需要重新生成邮箱重试 (API 模式失败)
|
||||
if register_success == "retry_new_email":
|
||||
log.warning(f"[Worker-{worker_id}] API 注册失败,重新生成邮箱重试...")
|
||||
with _tracker_lock:
|
||||
remove_account_from_tracker(_tracker, team_name, email)
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
# 创建新邮箱并邀请
|
||||
new_email, new_password = unified_create_email()
|
||||
if new_email and not is_email_blacklisted(new_email):
|
||||
if invite_single_to_team(new_email, _get_team_by_name(team_name)):
|
||||
with _tracker_lock:
|
||||
add_account_with_password(_tracker, team_name, new_email, new_password, "invited")
|
||||
save_team_tracker(_tracker)
|
||||
log.success(f"[Worker-{worker_id}] 已创建新邮箱: {new_email}")
|
||||
return result
|
||||
|
||||
if register_success and register_success != "domain_blacklisted":
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "registered")
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
if AUTH_PROVIDER == "s2a":
|
||||
from s2a_service import s2a_verify_account_in_pool
|
||||
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "authorized")
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
log.step(f"[Worker-{worker_id}] 验证 S2A 入库状态...")
|
||||
verified, account_data = s2a_verify_account_in_pool(email)
|
||||
|
||||
if verified:
|
||||
account_id = account_data.get("id", "")
|
||||
result["status"] = "success"
|
||||
result["crs_id"] = f"S2A-{account_id}"
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "completed")
|
||||
save_team_tracker(_tracker)
|
||||
log.success(f"[Worker-{worker_id}] ✅ S2A 入库成功: {email} (ID: {account_id})")
|
||||
else:
|
||||
log.warning(f"[Worker-{worker_id}] ⚠️ S2A 入库验证失败: {email}")
|
||||
result["status"] = "partial"
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "partial")
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
elif AUTH_PROVIDER == "cpa":
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "authorized")
|
||||
save_team_tracker(_tracker)
|
||||
result["status"] = "success"
|
||||
result["crs_id"] = "CPA-AUTO"
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "completed")
|
||||
save_team_tracker(_tracker)
|
||||
log.success(f"[Worker-{worker_id}] ✅ CPA 处理完成: {email}")
|
||||
|
||||
else:
|
||||
# CRS 模式
|
||||
if codex_data:
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "authorized")
|
||||
save_team_tracker(_tracker)
|
||||
crs_result = crs_add_account(email, codex_data)
|
||||
if crs_result:
|
||||
crs_id = crs_result.get("id", "")
|
||||
result["status"] = "success"
|
||||
result["crs_id"] = crs_id
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "completed")
|
||||
save_team_tracker(_tracker)
|
||||
log.success(f"[Worker-{worker_id}] ✅ CRS 入库成功: {email}")
|
||||
else:
|
||||
result["status"] = "partial"
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "partial")
|
||||
save_team_tracker(_tracker)
|
||||
else:
|
||||
result["status"] = "auth_failed"
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "auth_failed")
|
||||
save_team_tracker(_tracker)
|
||||
else:
|
||||
log.error(f"[Worker-{worker_id}] 注册/授权失败: {email}")
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "register_failed")
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
except ShutdownRequested:
|
||||
log.warning(f"[Worker-{worker_id}] 用户请求停止: {email}")
|
||||
with _tracker_lock:
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"[Worker-{worker_id}] 处理异常: {email} - {e}")
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "error")
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
# 保存到 CSV
|
||||
save_to_csv(
|
||||
email=email,
|
||||
password=password,
|
||||
team_name=team_name,
|
||||
status=result["status"],
|
||||
crs_id=result.get("crs_id", "")
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def process_accounts_concurrent(
|
||||
accounts: list,
|
||||
team_name: str,
|
||||
team_index: int = 0,
|
||||
teams_total: int = 0,
|
||||
include_owner: bool = False,
|
||||
max_workers: int = None
|
||||
) -> list:
|
||||
"""并发处理账号列表 (两阶段模式: 并行注册 + 串行授权)
|
||||
|
||||
阶段 1: 并行注册所有新账号
|
||||
阶段 2: 串行授权所有已注册的账号
|
||||
|
||||
Args:
|
||||
accounts: 账号列表 [{"email", "password", "status", "role"}]
|
||||
team_name: Team 名称
|
||||
team_index: 当前 Team 序号
|
||||
teams_total: Team 总数
|
||||
include_owner: 是否包含 Owner
|
||||
max_workers: 最大并发数 (默认使用配置值)
|
||||
|
||||
Returns:
|
||||
list: 处理结果
|
||||
"""
|
||||
global _tracker, _shutdown_requested
|
||||
|
||||
if max_workers is None:
|
||||
max_workers = CONCURRENT_WORKERS
|
||||
|
||||
stagger_delay = 3.0 # 线程错开启动间隔 (秒)
|
||||
|
||||
# 过滤已完成的账号
|
||||
pending_accounts = [acc for acc in accounts if acc.get("status") != "completed"]
|
||||
|
||||
if not pending_accounts:
|
||||
log.info("所有账号已完成,无需处理")
|
||||
return []
|
||||
|
||||
total = len(pending_accounts)
|
||||
actual_workers = min(max_workers, total)
|
||||
|
||||
# 分类账号: 需要注册的 vs 已注册待授权的
|
||||
need_register = []
|
||||
need_auth_only = []
|
||||
|
||||
for acc in pending_accounts:
|
||||
status = acc.get("status", "")
|
||||
role = acc.get("role", "member")
|
||||
|
||||
# 已注册但未授权的状态
|
||||
if status in ["registered", "auth_failed"] or \
|
||||
(AUTH_PROVIDER == "s2a" and status == "partial") or \
|
||||
(role == "owner" and status not in ["team_owner", "completed", "authorized", ""]):
|
||||
need_auth_only.append(acc)
|
||||
elif status == "team_owner":
|
||||
# Team Owner 使用 OTP,需要特殊处理
|
||||
need_auth_only.append(acc)
|
||||
elif status in ["invited", "processing", ""]:
|
||||
# 新账号,需要注册
|
||||
need_register.append(acc)
|
||||
else:
|
||||
# 其他状态,尝试注册
|
||||
need_register.append(acc)
|
||||
|
||||
log.section(f"两阶段并发处理 {total} 个账号")
|
||||
log.info(f"需要注册: {len(need_register)} 个, 需要授权: {len(need_auth_only)} 个")
|
||||
|
||||
# 启动进度跟踪
|
||||
progress_start(team_name, total, team_index, teams_total, include_owner)
|
||||
|
||||
results = []
|
||||
|
||||
# ==================== 阶段 1: 并行注册 ====================
|
||||
if need_register:
|
||||
log.section(f"阶段 1: 并行注册 {len(need_register)} 个账号 (并发数: {actual_workers})")
|
||||
|
||||
registered_accounts = []
|
||||
|
||||
with ThreadPoolExecutor(max_workers=actual_workers) as executor:
|
||||
future_to_account = {}
|
||||
|
||||
for i, account in enumerate(need_register):
|
||||
if _shutdown_requested:
|
||||
break
|
||||
|
||||
worker_id = i % actual_workers + 1
|
||||
log.info(f"[Worker-{worker_id}] 启动注册: {account['email']}", icon="start")
|
||||
|
||||
future = executor.submit(
|
||||
_register_single_account_worker,
|
||||
account,
|
||||
team_name,
|
||||
worker_id
|
||||
)
|
||||
future_to_account[future] = account
|
||||
|
||||
# 错开启动
|
||||
if i < len(need_register) - 1:
|
||||
time.sleep(stagger_delay)
|
||||
|
||||
# 收集注册结果
|
||||
for future in as_completed(future_to_account):
|
||||
if _shutdown_requested:
|
||||
log.warning("检测到中断请求,取消剩余任务...")
|
||||
executor.shutdown(wait=False, cancel_futures=True)
|
||||
break
|
||||
|
||||
account = future_to_account[future]
|
||||
try:
|
||||
reg_result = future.result()
|
||||
if reg_result == "success":
|
||||
log.success(f"✅ 注册成功: {account['email']}")
|
||||
registered_accounts.append(account)
|
||||
elif reg_result == "domain_blacklisted":
|
||||
log.error(f"❌ 域名黑名单: {account['email']}")
|
||||
domain = get_domain_from_email(account['email'])
|
||||
add_domain_to_blacklist(domain)
|
||||
with _tracker_lock:
|
||||
remove_account_from_tracker(_tracker, team_name, account['email'])
|
||||
save_team_tracker(_tracker)
|
||||
else:
|
||||
log.error(f"❌ 注册失败: {account['email']}")
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, account['email'], "register_failed")
|
||||
save_team_tracker(_tracker)
|
||||
except Exception as e:
|
||||
log.error(f"注册异常: {account.get('email', 'unknown')} - {e}")
|
||||
|
||||
log.success(f"阶段 1 完成: {len(registered_accounts)}/{len(need_register)} 注册成功")
|
||||
|
||||
# 将成功注册的账号加入授权列表
|
||||
need_auth_only.extend(registered_accounts)
|
||||
|
||||
# ==================== 阶段 2: 串行授权 ====================
|
||||
if need_auth_only and not _shutdown_requested:
|
||||
log.section(f"阶段 2: 串行授权 {len(need_auth_only)} 个账号")
|
||||
|
||||
for i, account in enumerate(need_auth_only):
|
||||
if _shutdown_requested:
|
||||
log.warning("检测到中断请求,停止授权...")
|
||||
break
|
||||
|
||||
email = account["email"]
|
||||
password = account["password"]
|
||||
role = account.get("role", "member")
|
||||
status = account.get("status", "")
|
||||
|
||||
log.info(f"[{i+1}/{len(need_auth_only)}] 授权: {email}", icon="auth")
|
||||
|
||||
result = {
|
||||
"team": team_name,
|
||||
"email": email,
|
||||
"password": password,
|
||||
"status": "failed",
|
||||
"crs_id": ""
|
||||
}
|
||||
|
||||
try:
|
||||
with Timer(f"授权 {email}"):
|
||||
# 判断授权方式
|
||||
if status == "team_owner":
|
||||
# Team Owner 使用 OTP
|
||||
log.info("Team Owner,使用 OTP 登录...", icon="auth")
|
||||
auth_success, codex_data = login_and_authorize_with_otp(email)
|
||||
else:
|
||||
# 普通账号使用密码登录授权
|
||||
auth_success, codex_data = authorize_only(email, password)
|
||||
|
||||
if auth_success:
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "authorized")
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
# 验证入库
|
||||
if AUTH_PROVIDER == "s2a":
|
||||
from s2a_service import s2a_verify_account_in_pool
|
||||
verified, account_data = s2a_verify_account_in_pool(email)
|
||||
|
||||
if verified:
|
||||
account_id = account_data.get("id", "")
|
||||
result["status"] = "success"
|
||||
result["crs_id"] = f"S2A-{account_id}"
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "completed")
|
||||
save_team_tracker(_tracker)
|
||||
log.success(f"✅ S2A 入库成功: {email} (ID: {account_id})")
|
||||
else:
|
||||
log.warning(f"⚠️ S2A 入库验证失败: {email}")
|
||||
result["status"] = "partial"
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "partial")
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
elif AUTH_PROVIDER == "cpa":
|
||||
result["status"] = "success"
|
||||
result["crs_id"] = "CPA-AUTO"
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "completed")
|
||||
save_team_tracker(_tracker)
|
||||
log.success(f"✅ CPA 处理完成: {email}")
|
||||
|
||||
else:
|
||||
# CRS 模式
|
||||
if codex_data:
|
||||
crs_result = crs_add_account(email, codex_data)
|
||||
if crs_result:
|
||||
crs_id = crs_result.get("id", "")
|
||||
result["status"] = "success"
|
||||
result["crs_id"] = crs_id
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "completed")
|
||||
save_team_tracker(_tracker)
|
||||
log.success(f"✅ CRS 入库成功: {email}")
|
||||
else:
|
||||
result["status"] = "partial"
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "partial")
|
||||
save_team_tracker(_tracker)
|
||||
else:
|
||||
log.error(f"❌ 授权失败: {email}")
|
||||
result["status"] = "auth_failed"
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "auth_failed")
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
except ShutdownRequested:
|
||||
log.warning(f"用户请求停止: {email}")
|
||||
with _tracker_lock:
|
||||
save_team_tracker(_tracker)
|
||||
break
|
||||
except Exception as e:
|
||||
log.error(f"授权异常: {email} - {e}")
|
||||
result["status"] = "error"
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "error")
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
# 保存到 CSV
|
||||
save_to_csv(
|
||||
email=email,
|
||||
password=password,
|
||||
team_name=team_name,
|
||||
status=result["status"],
|
||||
crs_id=result.get("crs_id", "")
|
||||
)
|
||||
|
||||
results.append(result)
|
||||
|
||||
# 更新进度
|
||||
is_success = result["status"] in ("success", "completed")
|
||||
progress_account_done(email, is_success)
|
||||
|
||||
# 授权间隔
|
||||
if i < len(need_auth_only) - 1 and not _shutdown_requested:
|
||||
time.sleep(1)
|
||||
|
||||
# 统计结果
|
||||
success_count = sum(1 for r in results if r["status"] in ("success", "completed"))
|
||||
log.success(f"两阶段处理完成: {success_count}/{len(results)} 成功")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _register_single_account_worker(account: dict, team_name: str, worker_id: int) -> str:
|
||||
"""单个账号注册工作函数 (用于阶段 1 并行注册)
|
||||
|
||||
Args:
|
||||
account: 账号信息
|
||||
team_name: Team 名称
|
||||
worker_id: 工作线程 ID
|
||||
|
||||
Returns:
|
||||
str: "success", "domain_blacklisted", or "failed"
|
||||
"""
|
||||
global _tracker, _shutdown_requested
|
||||
|
||||
email = account["email"]
|
||||
password = account["password"]
|
||||
|
||||
# 检查中断请求
|
||||
if _shutdown_requested:
|
||||
return "failed"
|
||||
|
||||
# 检查邮箱域名黑名单
|
||||
if is_email_blacklisted(email):
|
||||
return "domain_blacklisted"
|
||||
|
||||
log.info(f"[Worker-{worker_id}] 开始注册: {email}", icon="account")
|
||||
|
||||
# 标记为处理中
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "processing")
|
||||
save_team_tracker(_tracker)
|
||||
|
||||
try:
|
||||
with Timer(f"[Worker-{worker_id}] 注册 {email}"):
|
||||
result = register_only(email, password)
|
||||
|
||||
if result == "success":
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "registered")
|
||||
save_team_tracker(_tracker)
|
||||
return "success"
|
||||
elif result == "domain_blacklisted":
|
||||
return "domain_blacklisted"
|
||||
else:
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "register_failed")
|
||||
save_team_tracker(_tracker)
|
||||
return "failed"
|
||||
|
||||
except ShutdownRequested:
|
||||
return "failed"
|
||||
except Exception as e:
|
||||
log.error(f"[Worker-{worker_id}] 注册异常: {email} - {e}")
|
||||
with _tracker_lock:
|
||||
update_account_status(_tracker, team_name, email, "error")
|
||||
save_team_tracker(_tracker)
|
||||
return "failed"
|
||||
|
||||
|
||||
def _print_system_config():
|
||||
"""打印当前系统配置"""
|
||||
from config import (
|
||||
EMAIL_PROVIDER, AUTH_PROVIDER, ACCOUNTS_PER_TEAM,
|
||||
INCLUDE_TEAM_OWNERS, BROWSER_RANDOM_FINGERPRINT,
|
||||
S2A_API_BASE, CPA_API_BASE, CRS_API_BASE,
|
||||
PROXY_ENABLED, PROXIES
|
||||
PROXY_ENABLED, PROXIES,
|
||||
CONCURRENT_ENABLED, CONCURRENT_WORKERS
|
||||
)
|
||||
|
||||
log.section("系统配置")
|
||||
@@ -560,6 +1174,12 @@ def _print_system_config():
|
||||
log.info(f"Owner 入库: {'✓ 开启' if INCLUDE_TEAM_OWNERS else '✗ 关闭'}", icon="config")
|
||||
log.info(f"随机指纹: {'✓ 开启' if BROWSER_RANDOM_FINGERPRINT else '✗ 关闭'}", icon="config")
|
||||
|
||||
# 并发配置
|
||||
if CONCURRENT_ENABLED:
|
||||
log.info(f"并发处理: ✓ 开启 ({CONCURRENT_WORKERS} 并发, 授权串行)", icon="config")
|
||||
else:
|
||||
log.info("并发处理: ✗ 关闭 (串行模式)", icon="config")
|
||||
|
||||
if PROXY_ENABLED and PROXIES:
|
||||
log.info(f"代理: 已启用 ({len(PROXIES)} 个)", icon="proxy")
|
||||
else:
|
||||
@@ -591,26 +1211,59 @@ def run_all_teams():
|
||||
log.warning(f"发现 {total_incomplete} 个未完成账号,将优先处理")
|
||||
|
||||
_current_results = []
|
||||
teams_total = len(TEAMS)
|
||||
|
||||
# 筛选需要处理的 Team (有未完成账号或还没开始处理的)
|
||||
teams_to_process = []
|
||||
for i, team in enumerate(TEAMS):
|
||||
team_name = team["name"]
|
||||
team_accounts = _tracker.get("teams", {}).get(team_name, [])
|
||||
member_accounts = [acc for acc in team_accounts if acc.get("role") != "owner"]
|
||||
owner_accounts = [acc for acc in team_accounts if acc.get("role") == "owner" and acc.get("status") != "completed"]
|
||||
|
||||
completed_count = sum(1 for acc in member_accounts if acc.get("status") == "completed")
|
||||
member_count = len(member_accounts)
|
||||
|
||||
# 需要处理的条件:
|
||||
# 1. 成员数量未达标
|
||||
# 2. 有未完成的成员
|
||||
# 3. 有未完成的 Owner
|
||||
needs_processing = (
|
||||
member_count < ACCOUNTS_PER_TEAM or
|
||||
completed_count < member_count or
|
||||
len(owner_accounts) > 0
|
||||
)
|
||||
|
||||
if needs_processing:
|
||||
teams_to_process.append((i, team))
|
||||
|
||||
if not teams_to_process:
|
||||
log.success("所有 Team 已完成处理,无需继续")
|
||||
return _current_results
|
||||
|
||||
skipped_count = len(TEAMS) - len(teams_to_process)
|
||||
if skipped_count > 0:
|
||||
log.info(f"跳过 {skipped_count} 个已完成的 Team,处理剩余 {len(teams_to_process)} 个")
|
||||
|
||||
teams_total = len(teams_to_process)
|
||||
|
||||
with Timer("全部流程"):
|
||||
# ========== 处理所有 Team (成员 + Owner 一起) ==========
|
||||
for i, team in enumerate(TEAMS):
|
||||
# ========== 处理需要处理的 Team (成员 + Owner 一起) ==========
|
||||
for idx, (original_idx, team) in enumerate(teams_to_process):
|
||||
if _shutdown_requested:
|
||||
log.warning("检测到中断请求,停止处理...")
|
||||
break
|
||||
|
||||
log.separator("★", 60)
|
||||
team_email = team.get('account') or team.get('owner_email', '')
|
||||
log.highlight(f"Team {i + 1}/{teams_total}: {team['name']} ({team_email})", icon="team")
|
||||
log.highlight(f"Team {idx + 1}/{teams_total}: {team['name']} ({team_email})", icon="team")
|
||||
log.separator("★", 60)
|
||||
|
||||
# 传递 Team 序号信息
|
||||
results, _ = process_single_team(team, team_index=i + 1, teams_total=teams_total)
|
||||
results, _ = process_single_team(team, team_index=idx + 1, teams_total=teams_total)
|
||||
_current_results.extend(results)
|
||||
|
||||
# Team 之间的间隔
|
||||
if i < teams_total - 1 and not _shutdown_requested:
|
||||
if idx < teams_total - 1 and not _shutdown_requested:
|
||||
wait_time = 3
|
||||
log.countdown(wait_time, "下一个 Team")
|
||||
|
||||
@@ -645,6 +1298,85 @@ def run_single_team(team_index: int = 0):
|
||||
return _current_results
|
||||
|
||||
|
||||
def run_teams_by_count(count: int):
|
||||
"""运行指定数量的 Team
|
||||
|
||||
Args:
|
||||
count: 要处理的 Team 数量
|
||||
"""
|
||||
global _tracker, _current_results, _shutdown_requested
|
||||
|
||||
log.header("ChatGPT Team 批量注册自动化")
|
||||
|
||||
# 打印系统配置
|
||||
_print_system_config()
|
||||
|
||||
# 限制数量不超过总数
|
||||
actual_count = min(count, len(TEAMS))
|
||||
|
||||
log.info(f"选择处理前 {actual_count} 个 Team (共 {len(TEAMS)} 个)", icon="team")
|
||||
log.info(f"统一密码: {DEFAULT_PASSWORD}", icon="code")
|
||||
log.info("按 Ctrl+C 可安全退出并保存进度")
|
||||
log.separator()
|
||||
|
||||
# 先显示整体状态
|
||||
_tracker = load_team_tracker()
|
||||
|
||||
_current_results = []
|
||||
|
||||
# 筛选需要处理的 Team (只取前 count 个中需要处理的)
|
||||
teams_to_process = []
|
||||
for i, team in enumerate(TEAMS[:actual_count]):
|
||||
team_name = team["name"]
|
||||
team_accounts = _tracker.get("teams", {}).get(team_name, [])
|
||||
member_accounts = [acc for acc in team_accounts if acc.get("role") != "owner"]
|
||||
owner_accounts = [acc for acc in team_accounts if acc.get("role") == "owner" and acc.get("status") != "completed"]
|
||||
|
||||
completed_count = sum(1 for acc in member_accounts if acc.get("status") == "completed")
|
||||
member_count = len(member_accounts)
|
||||
|
||||
needs_processing = (
|
||||
member_count < ACCOUNTS_PER_TEAM or
|
||||
completed_count < member_count or
|
||||
len(owner_accounts) > 0
|
||||
)
|
||||
|
||||
if needs_processing:
|
||||
teams_to_process.append((i, team))
|
||||
|
||||
if not teams_to_process:
|
||||
log.success("选定的 Team 已全部完成处理,无需继续")
|
||||
return _current_results
|
||||
|
||||
skipped_count = actual_count - len(teams_to_process)
|
||||
if skipped_count > 0:
|
||||
log.info(f"跳过 {skipped_count} 个已完成的 Team,处理剩余 {len(teams_to_process)} 个")
|
||||
|
||||
teams_total = len(teams_to_process)
|
||||
|
||||
with Timer("全部流程"):
|
||||
for idx, (original_idx, team) in enumerate(teams_to_process):
|
||||
if _shutdown_requested:
|
||||
log.warning("检测到中断请求,停止处理...")
|
||||
break
|
||||
|
||||
log.separator("★", 60)
|
||||
team_email = team.get('account') or team.get('owner_email', '')
|
||||
log.highlight(f"Team {idx + 1}/{teams_total}: {team['name']} ({team_email})", icon="team")
|
||||
log.separator("★", 60)
|
||||
|
||||
results, _ = process_single_team(team, team_index=idx + 1, teams_total=teams_total)
|
||||
_current_results.extend(results)
|
||||
|
||||
if idx < teams_total - 1 and not _shutdown_requested:
|
||||
wait_time = 3
|
||||
log.countdown(wait_time, "下一个 Team")
|
||||
|
||||
print_summary(_current_results)
|
||||
|
||||
return _current_results
|
||||
|
||||
|
||||
def test_email_only():
|
||||
"""测试模式: 只创建邮箱和邀请,不注册"""
|
||||
global _tracker
|
||||
|
||||
481
s2a_service.py
481
s2a_service.py
@@ -6,12 +6,24 @@
|
||||
# - 会话标识: S2A 使用 session_id
|
||||
# - 授权流程: S2A 生成授权 URL -> 用户授权 -> 提交 code 换取 token -> 创建账号
|
||||
# - 账号入库: S2A 可一步完成 (create-from-oauth) 或分步完成 (exchange + add_account)
|
||||
#
|
||||
# 新增: 纯 API 授权模式 (无需浏览器)
|
||||
# - 使用 curl_cffi 模拟浏览器指纹
|
||||
# - 支持 Sentinel PoW 验证
|
||||
# - 直接通过 API 完成 OAuth 流程
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from typing import Optional, Tuple, Dict, List, Any
|
||||
import json
|
||||
import uuid
|
||||
import time
|
||||
import random
|
||||
import base64
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from config import (
|
||||
S2A_API_BASE,
|
||||
@@ -23,6 +35,8 @@ from config import (
|
||||
S2A_GROUP_NAMES,
|
||||
REQUEST_TIMEOUT,
|
||||
USER_AGENT,
|
||||
get_next_proxy,
|
||||
format_proxy_url,
|
||||
)
|
||||
from logger import log
|
||||
|
||||
@@ -49,6 +63,349 @@ def create_session_with_retry() -> requests.Session:
|
||||
http_session = create_session_with_retry()
|
||||
|
||||
|
||||
# ==================== PoW Solver (Sentinel 验证) ====================
|
||||
|
||||
def _fnv1a_32(data: bytes) -> int:
|
||||
"""FNV-1a 32-bit hash"""
|
||||
h = 2166136261
|
||||
for byte in data:
|
||||
h ^= byte
|
||||
h = (h * 16777619) & 0xFFFFFFFF
|
||||
h ^= (h >> 16)
|
||||
h = (h * 2246822507) & 0xFFFFFFFF
|
||||
h ^= (h >> 13)
|
||||
h = (h * 3266489909) & 0xFFFFFFFF
|
||||
h ^= (h >> 16)
|
||||
return h
|
||||
|
||||
|
||||
def _get_parse_time() -> str:
|
||||
"""生成 JS Date().toString() 格式的时间戳"""
|
||||
now = datetime.now(timezone(timedelta(hours=8)))
|
||||
return now.strftime("%a %b %d %Y %H:%M:%S") + " GMT+0800 (中国标准时间)"
|
||||
|
||||
|
||||
def _get_pow_config(user_agent: str, sid: str = None) -> list:
|
||||
"""生成 PoW 配置数组"""
|
||||
if not sid:
|
||||
sid = str(uuid.uuid4())
|
||||
return [
|
||||
random.randint(2500, 3500),
|
||||
_get_parse_time(),
|
||||
4294967296,
|
||||
0,
|
||||
user_agent,
|
||||
"chrome-extension://pgojnojmmhpofjgdmaebadhbocahppod/assets/aW5qZWN0X2hhc2g/aW5qZ",
|
||||
None,
|
||||
"zh-CN",
|
||||
"zh-CN",
|
||||
0,
|
||||
f"canShare−function canShare() {{ [native code] }}",
|
||||
f"_reactListening{random.randint(1000000, 9999999)}",
|
||||
"onhashchange",
|
||||
time.perf_counter() * 1000,
|
||||
sid,
|
||||
"",
|
||||
24,
|
||||
int(time.time() * 1000 - random.randint(10000, 50000))
|
||||
]
|
||||
|
||||
|
||||
def _solve_pow(seed: str, difficulty: str, config: list, max_iterations: int = 5000000) -> Optional[str]:
|
||||
"""CPU 求解 PoW"""
|
||||
start_time = time.perf_counter()
|
||||
seed_bytes = seed.encode()
|
||||
|
||||
for iteration in range(max_iterations):
|
||||
config[3] = iteration
|
||||
config[9] = 0
|
||||
|
||||
json_str = json.dumps(config, separators=(',', ':'))
|
||||
encoded = base64.b64encode(json_str.encode())
|
||||
|
||||
h = _fnv1a_32(seed_bytes + encoded)
|
||||
hex_hash = f"{h:08x}"
|
||||
|
||||
if hex_hash[:len(difficulty)] <= difficulty:
|
||||
elapsed = time.perf_counter() - start_time
|
||||
log.debug(f"[PoW] 求解完成: {elapsed:.2f}s (迭代 {iteration:,}, 难度={difficulty})")
|
||||
return f"{encoded.decode()}~S"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_requirements_token(user_agent: str, sid: str = None) -> str:
|
||||
"""生成 requirements token"""
|
||||
if not sid:
|
||||
sid = str(uuid.uuid4())
|
||||
config = _get_pow_config(user_agent, sid)
|
||||
config[3] = 0
|
||||
config[9] = 0
|
||||
json_str = json.dumps(config, separators=(',', ':'))
|
||||
encoded = base64.b64encode(json_str.encode()).decode()
|
||||
return f"gAAAAAC{encoded}~S"
|
||||
|
||||
|
||||
# ==================== S2A API 授权器 ====================
|
||||
|
||||
class S2AApiAuthorizer:
|
||||
"""S2A 纯 API 授权器 - 无需浏览器"""
|
||||
|
||||
def __init__(self, email: str, password: str, proxy: str = None):
|
||||
self.email = email
|
||||
self.password = password
|
||||
|
||||
# 尝试导入 curl_cffi,如果失败则使用 requests
|
||||
try:
|
||||
from curl_cffi import requests as cffi_requests
|
||||
self.session = cffi_requests.Session(impersonate="chrome110")
|
||||
self._use_cffi = True
|
||||
except ImportError:
|
||||
log.warning("curl_cffi 未安装,使用 requests (可能被检测)")
|
||||
self.session = requests.Session()
|
||||
self._use_cffi = False
|
||||
|
||||
if proxy:
|
||||
self.session.proxies = {"http": proxy, "https": proxy}
|
||||
|
||||
self.ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
|
||||
self.sid = str(uuid.uuid4())
|
||||
self.device_id = str(uuid.uuid4())
|
||||
self.sentinel_token = None
|
||||
self.solved_pow = None
|
||||
|
||||
self.session.headers.update({
|
||||
"User-Agent": self.ua,
|
||||
"Accept": "*/*",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"sec-ch-ua": '"Not(A:Brand";v="8", "Chromium";v="144", "Microsoft Edge";v="144"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"Windows"',
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-origin",
|
||||
})
|
||||
|
||||
def _call_sentinel_req(self, flow: str) -> Optional[dict]:
|
||||
"""调用 sentinel 获取 token 和处理 PoW"""
|
||||
init_token = _get_requirements_token(self.ua, self.sid)
|
||||
payload = {"p": init_token, "id": self.device_id, "flow": flow}
|
||||
|
||||
try:
|
||||
resp = self.session.post(
|
||||
"https://sentinel.openai.com/backend-api/sentinel/req",
|
||||
json=payload,
|
||||
timeout=15
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
log.warning(f"Sentinel 请求失败: {resp.status_code}")
|
||||
return None
|
||||
|
||||
data = resp.json()
|
||||
self.sentinel_token = data.get('token')
|
||||
pow_req = data.get('proofofwork', {})
|
||||
|
||||
if pow_req.get('required'):
|
||||
seed = pow_req.get('seed', '')
|
||||
difficulty = pow_req.get('difficulty', '')
|
||||
config = _get_pow_config(self.ua, self.sid)
|
||||
solved = _solve_pow(seed, difficulty, config)
|
||||
if solved:
|
||||
self.solved_pow = f"gAAAAAB{solved}"
|
||||
else:
|
||||
log.error("PoW 求解失败")
|
||||
return None
|
||||
else:
|
||||
self.solved_pow = init_token
|
||||
return data
|
||||
except Exception as e:
|
||||
log.error(f"Sentinel 异常: {e}")
|
||||
return None
|
||||
|
||||
def _get_sentinel_header(self, header_flow: str) -> str:
|
||||
"""构建 sentinel header"""
|
||||
sentinel_obj = {"p": self.solved_pow, "id": self.device_id, "flow": header_flow}
|
||||
if self.sentinel_token:
|
||||
sentinel_obj["c"] = self.sentinel_token
|
||||
return json.dumps(sentinel_obj)
|
||||
|
||||
def get_authorization_code(self, auth_url: str) -> Optional[str]:
|
||||
"""执行 OAuth 流程,返回 authorization code
|
||||
|
||||
Args:
|
||||
auth_url: S2A 生成的授权 URL
|
||||
|
||||
Returns:
|
||||
str: 授权码 或 None
|
||||
"""
|
||||
log.step("开始 API 授权流程...")
|
||||
|
||||
headers = {
|
||||
"Origin": "https://auth.openai.com",
|
||||
"Referer": "https://auth.openai.com/log-in",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
try:
|
||||
# 1. 访问授权端点
|
||||
log.step("访问授权端点...")
|
||||
resp = self.session.get(auth_url, allow_redirects=True)
|
||||
headers["Referer"] = resp.url
|
||||
|
||||
# 2. 提交邮箱
|
||||
log.step("提交邮箱...")
|
||||
if not self._call_sentinel_req("login_web_init"):
|
||||
return None
|
||||
|
||||
auth_headers = headers.copy()
|
||||
auth_headers["OpenAI-Sentinel-Token"] = self._get_sentinel_header("authorize_continue")
|
||||
resp = self.session.post(
|
||||
"https://auth.openai.com/api/accounts/authorize/continue",
|
||||
json={"username": {"kind": "email", "value": self.email}},
|
||||
headers=auth_headers
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
log.error(f"邮箱提交失败: {resp.status_code} - {resp.text[:200]}")
|
||||
return None
|
||||
|
||||
data = resp.json()
|
||||
page_type = data.get("page", {}).get("type", "")
|
||||
|
||||
# 3. 验证密码
|
||||
if page_type == "password" or "password" in str(data):
|
||||
log.step("验证密码...")
|
||||
if not self._call_sentinel_req("authorize_continue__auto"):
|
||||
return None
|
||||
|
||||
verify_headers = headers.copy()
|
||||
verify_headers["OpenAI-Sentinel-Token"] = self._get_sentinel_header("password_verify")
|
||||
resp = self.session.post(
|
||||
"https://auth.openai.com/api/accounts/password/verify",
|
||||
json={"username": self.email, "password": self.password},
|
||||
headers=verify_headers
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
log.error(f"密码验证失败: {resp.status_code} - {resp.text[:200]}")
|
||||
return None
|
||||
|
||||
# 4. 获取 continue_url (无需选择 workspace,S2A 授权链接已包含)
|
||||
data = resp.json()
|
||||
continue_url = data.get("continue_url")
|
||||
|
||||
# 如果没有 continue_url,可能需要额外的 sentinel 调用
|
||||
if not continue_url:
|
||||
log.step("获取重定向 URL...")
|
||||
if not self._call_sentinel_req("password_verify__auto"):
|
||||
return None
|
||||
|
||||
# 尝试再次获取
|
||||
resp = self.session.post(
|
||||
"https://auth.openai.com/api/accounts/authorize/continue",
|
||||
json={},
|
||||
headers=auth_headers
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
continue_url = data.get("continue_url")
|
||||
|
||||
if not continue_url:
|
||||
log.error(f"无法获取 continue_url: {data}")
|
||||
return None
|
||||
|
||||
# 5. 跟踪重定向直到获取 code
|
||||
log.step("跟踪重定向...")
|
||||
for _ in range(10):
|
||||
resp = self.session.get(continue_url, allow_redirects=False)
|
||||
if resp.status_code in (301, 302, 303, 307, 308):
|
||||
location = resp.headers.get('Location', '')
|
||||
if "localhost:1455" in location:
|
||||
parsed = urlparse(location)
|
||||
query = parse_qs(parsed.query)
|
||||
code = query.get('code', [None])[0]
|
||||
if code:
|
||||
log.success("成功获取授权码")
|
||||
return code
|
||||
continue_url = location
|
||||
else:
|
||||
break
|
||||
|
||||
log.error("无法获取授权码")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"API 授权异常: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
|
||||
def s2a_api_authorize(
|
||||
email: str,
|
||||
password: str,
|
||||
proxy: str = None
|
||||
) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
||||
"""S2A 纯 API 授权 (无需浏览器)
|
||||
|
||||
使用 OpenAI 认证 API 直接完成授权流程,无需浏览器自动化。
|
||||
|
||||
Args:
|
||||
email: 账号邮箱
|
||||
password: 账号密码
|
||||
proxy: 代理地址 (可选,格式: http://host:port 或 socks5://user:pass@host:port)
|
||||
|
||||
Returns:
|
||||
tuple: (是否成功, 账号数据或None)
|
||||
"""
|
||||
if not S2A_API_BASE or (not S2A_ADMIN_KEY and not S2A_ADMIN_TOKEN):
|
||||
log.error("S2A 未配置")
|
||||
return False, None
|
||||
|
||||
# 使用配置的代理
|
||||
if not proxy:
|
||||
proxy_config = get_next_proxy()
|
||||
if proxy_config:
|
||||
proxy = format_proxy_url(proxy_config)
|
||||
|
||||
log.info(f"开始 S2A API 授权: {email}", icon="code")
|
||||
if proxy:
|
||||
log.debug(f"使用代理: {proxy[:30]}...")
|
||||
|
||||
try:
|
||||
# 1. 生成授权 URL
|
||||
auth_url, session_id = s2a_generate_auth_url()
|
||||
if not auth_url or not session_id:
|
||||
log.error("无法获取 S2A 授权 URL")
|
||||
return False, None
|
||||
|
||||
log.debug(f"授权 URL: {auth_url[:80]}...")
|
||||
log.debug(f"Session ID: {session_id[:16]}...")
|
||||
|
||||
# 2. 使用 API 授权器获取 code
|
||||
authorizer = S2AApiAuthorizer(email, password, proxy)
|
||||
code = authorizer.get_authorization_code(auth_url)
|
||||
|
||||
if not code:
|
||||
log.error("API 授权失败,无法获取授权码")
|
||||
return False, None
|
||||
|
||||
log.debug(f"授权码: {code[:20]}...")
|
||||
|
||||
# 3. 提交授权码创建账号
|
||||
log.step("提交授权码到 S2A...")
|
||||
result = s2a_create_account_from_oauth(code, session_id, name=email)
|
||||
|
||||
if result:
|
||||
log.success(f"S2A API 授权成功: {email}")
|
||||
return True, result
|
||||
else:
|
||||
log.error("S2A 账号入库失败")
|
||||
return False, None
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"S2A API 授权异常: {e}")
|
||||
return False, None
|
||||
|
||||
|
||||
def build_s2a_headers() -> Dict[str, str]:
|
||||
"""构建 S2A API 请求的 Headers
|
||||
|
||||
@@ -353,7 +710,7 @@ def s2a_create_account_from_oauth(
|
||||
full_email = name if "@" in name else ""
|
||||
|
||||
if name:
|
||||
payload["name"] = name
|
||||
payload["name"] = name if name.startswith("team-") else f"team-{name}"
|
||||
if proxy_id is not None:
|
||||
payload["proxy_id"] = proxy_id
|
||||
|
||||
@@ -433,8 +790,9 @@ def s2a_add_account(
|
||||
if token_info.get("email"):
|
||||
credentials["email"] = token_info.get("email")
|
||||
|
||||
s2a_name = name if name.startswith("team-") else f"team-{name}"
|
||||
payload = {
|
||||
"name": name,
|
||||
"name": s2a_name,
|
||||
"platform": "openai",
|
||||
"type": "oauth",
|
||||
"credentials": credentials,
|
||||
@@ -1211,3 +1569,122 @@ def format_keys_usage(keys: List[Dict[str, Any]], period_text: str = "今日") -
|
||||
lines.append(f" 费用: {fmt_cost(total_cost)}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ==================== 批量 API 授权 ====================
|
||||
|
||||
def s2a_batch_api_authorize(
|
||||
accounts: List[Dict[str, str]],
|
||||
proxy: str = None,
|
||||
progress_callback: Optional[callable] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""批量使用 API 模式授权账号到 S2A
|
||||
|
||||
无需浏览器,直接通过 OpenAI 认证 API 完成授权。
|
||||
|
||||
Args:
|
||||
accounts: 账号列表 [{"email": "xxx", "password": "xxx"}, ...]
|
||||
proxy: 代理地址 (可选)
|
||||
progress_callback: 进度回调函数 (current, total, email, status, message)
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
"success": int,
|
||||
"failed": int,
|
||||
"total": int,
|
||||
"details": [{"email": "xxx", "status": "success/failed", "message": "xxx"}, ...]
|
||||
}
|
||||
"""
|
||||
results = {
|
||||
"success": 0,
|
||||
"failed": 0,
|
||||
"total": len(accounts),
|
||||
"details": []
|
||||
}
|
||||
|
||||
if not S2A_API_BASE or (not S2A_ADMIN_KEY and not S2A_ADMIN_TOKEN):
|
||||
log.error("S2A 未配置")
|
||||
return results
|
||||
|
||||
# 使用配置的代理
|
||||
if not proxy:
|
||||
proxy_config = get_next_proxy()
|
||||
if proxy_config:
|
||||
proxy = format_proxy_url(proxy_config)
|
||||
|
||||
log.info(f"开始批量 API 授权: {len(accounts)} 个账号")
|
||||
|
||||
for i, acc in enumerate(accounts):
|
||||
email = acc.get("email", "")
|
||||
password = acc.get("password", "")
|
||||
|
||||
if not email or not password:
|
||||
results["failed"] += 1
|
||||
results["details"].append({
|
||||
"email": email or "unknown",
|
||||
"status": "failed",
|
||||
"message": "缺少邮箱或密码"
|
||||
})
|
||||
if progress_callback:
|
||||
progress_callback(i + 1, len(accounts), email, "failed", "缺少邮箱或密码")
|
||||
continue
|
||||
|
||||
try:
|
||||
success, result = s2a_api_authorize(email, password, proxy)
|
||||
|
||||
if success:
|
||||
results["success"] += 1
|
||||
account_id = result.get("id", "") if result else ""
|
||||
results["details"].append({
|
||||
"email": email,
|
||||
"status": "success",
|
||||
"message": f"ID: {account_id}"
|
||||
})
|
||||
if progress_callback:
|
||||
progress_callback(i + 1, len(accounts), email, "success", f"ID: {account_id}")
|
||||
else:
|
||||
results["failed"] += 1
|
||||
results["details"].append({
|
||||
"email": email,
|
||||
"status": "failed",
|
||||
"message": "授权失败"
|
||||
})
|
||||
if progress_callback:
|
||||
progress_callback(i + 1, len(accounts), email, "failed", "授权失败")
|
||||
|
||||
except Exception as e:
|
||||
results["failed"] += 1
|
||||
results["details"].append({
|
||||
"email": email,
|
||||
"status": "failed",
|
||||
"message": str(e)
|
||||
})
|
||||
if progress_callback:
|
||||
progress_callback(i + 1, len(accounts), email, "failed", str(e))
|
||||
|
||||
log.success(f"批量授权完成: 成功 {results['success']}, 失败 {results['failed']}")
|
||||
return results
|
||||
|
||||
|
||||
def s2a_api_authorize_single(
|
||||
email: str,
|
||||
password: str,
|
||||
proxy: str = None
|
||||
) -> Tuple[bool, str]:
|
||||
"""单个账号 API 授权 (简化返回值)
|
||||
|
||||
Args:
|
||||
email: 账号邮箱
|
||||
password: 账号密码
|
||||
proxy: 代理地址 (可选)
|
||||
|
||||
Returns:
|
||||
tuple: (是否成功, 消息)
|
||||
"""
|
||||
success, result = s2a_api_authorize(email, password, proxy)
|
||||
|
||||
if success:
|
||||
account_id = result.get("id", "") if result else ""
|
||||
return True, f"授权成功 (ID: {account_id})"
|
||||
else:
|
||||
return False, "授权失败"
|
||||
|
||||
418
stripe_api.py
Normal file
418
stripe_api.py
Normal file
@@ -0,0 +1,418 @@
|
||||
"""
|
||||
Stripe SEPA 支付 API 模块
|
||||
- 使用纯 API 方式完成 Stripe SEPA 支付
|
||||
- 参考 team-reg-go/stripe/stripe.go 实现
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
import random
|
||||
|
||||
try:
|
||||
from curl_cffi import requests as curl_requests
|
||||
CURL_CFFI_AVAILABLE = True
|
||||
except ImportError:
|
||||
CURL_CFFI_AVAILABLE = False
|
||||
curl_requests = None
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def log_status(step, message):
|
||||
"""日志输出"""
|
||||
timestamp = time.strftime("%H:%M:%S")
|
||||
print(f"[{timestamp}] [{step}] {message}")
|
||||
|
||||
|
||||
def log_progress(message):
|
||||
"""进度输出"""
|
||||
print(f" {message}")
|
||||
|
||||
|
||||
# Stripe 配置常量 (与 team-reg-go 保持一致)
|
||||
STRIPE_VERSION = "2020-08-27;custom_checkout_beta=v1"
|
||||
STRIPE_JS_VERSION = "c8cd270e71"
|
||||
OPENAI_STRIPE_PUBLIC_KEY = "pk_live_51Pj377KslHRdbaPgTJYjThzH3f5dt1N1vK7LUp0qh0yNSarhfZ6nfbG7FFlh8KLxVkvdMWN5o6Mc4Vda6NHaSnaV00C2Sbl8Zs"
|
||||
|
||||
|
||||
def generate_stripe_fingerprint() -> str:
|
||||
"""生成 Stripe 指纹 ID"""
|
||||
return uuid.uuid4().hex
|
||||
|
||||
|
||||
def extract_session_id(checkout_url: str) -> str:
|
||||
"""从 checkout URL 提取 session_id"""
|
||||
match = re.search(r'(cs_(?:live|test)_[a-zA-Z0-9]+)', checkout_url)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
class StripePaymentAPI:
|
||||
"""Stripe SEPA 支付 API 处理器"""
|
||||
|
||||
def __init__(self, checkout_url: str, session=None, proxy: str = None):
|
||||
"""初始化
|
||||
|
||||
Args:
|
||||
checkout_url: Stripe checkout URL
|
||||
session: 可选的 curl_cffi session (复用已有会话)
|
||||
proxy: 代理地址
|
||||
"""
|
||||
self.checkout_url = checkout_url
|
||||
self.session_id = extract_session_id(checkout_url)
|
||||
self.stripe_public_key = OPENAI_STRIPE_PUBLIC_KEY
|
||||
self.init_checksum = ""
|
||||
self.js_checksum = ""
|
||||
self.guid = generate_stripe_fingerprint()
|
||||
self.muid = generate_stripe_fingerprint()
|
||||
self.sid = generate_stripe_fingerprint()
|
||||
self.client_session_id = ""
|
||||
self.checkout_config_id = ""
|
||||
|
||||
# 创建或复用 session
|
||||
if session:
|
||||
self.session = session
|
||||
elif CURL_CFFI_AVAILABLE:
|
||||
self.session = curl_requests.Session(
|
||||
impersonate="chrome",
|
||||
verify=False,
|
||||
proxies={"http": proxy, "https": proxy} if proxy else {}
|
||||
)
|
||||
else:
|
||||
self.session = requests.Session()
|
||||
if proxy:
|
||||
self.session.proxies = {"http": proxy, "https": proxy}
|
||||
|
||||
def _get_stripe_headers(self) -> dict:
|
||||
"""获取 Stripe API 请求头"""
|
||||
return {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json",
|
||||
"Origin": "https://pay.openai.com",
|
||||
"Referer": "https://pay.openai.com/",
|
||||
"Sec-Fetch-Site": "cross-site",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Ch-Ua-Platform": '"Windows"',
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||
}
|
||||
|
||||
def fetch_checkout_page(self) -> bool:
|
||||
"""获取 checkout 页面参数"""
|
||||
try:
|
||||
headers = {
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||
}
|
||||
resp = self.session.get(self.checkout_url, headers=headers, timeout=30)
|
||||
|
||||
if resp.status_code == 200:
|
||||
html = resp.text
|
||||
|
||||
# 提取 initChecksum
|
||||
init_match = re.search(r'"initChecksum":\s*"([^"]+)"', html)
|
||||
if init_match:
|
||||
self.init_checksum = init_match.group(1)
|
||||
|
||||
# 提取 jsChecksum
|
||||
js_match = re.search(r'"jsChecksum":\s*"([^"]+)"', html)
|
||||
if js_match:
|
||||
self.js_checksum = js_match.group(1)
|
||||
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
log_progress(f"[!] 获取 checkout 页面失败: {e}")
|
||||
return False
|
||||
|
||||
def create_payment_method(self, iban: str, name: str, email: str,
|
||||
address: str, city: str, postal_code: str,
|
||||
country: str = "DE") -> str:
|
||||
"""Step 1: 创建支付方式
|
||||
|
||||
Returns:
|
||||
str: payment_method_id,失败返回空字符串
|
||||
"""
|
||||
api_url = "https://api.stripe.com/v1/payment_methods"
|
||||
|
||||
self.client_session_id = str(uuid.uuid4())
|
||||
self.checkout_config_id = str(uuid.uuid4())
|
||||
|
||||
data = {
|
||||
"type": "sepa_debit",
|
||||
"sepa_debit[iban]": iban,
|
||||
"billing_details[name]": name,
|
||||
"billing_details[email]": email,
|
||||
"billing_details[address][country]": country,
|
||||
"billing_details[address][line1]": address,
|
||||
"billing_details[address][city]": city,
|
||||
"billing_details[address][postal_code]": postal_code,
|
||||
"guid": self.guid,
|
||||
"muid": self.muid,
|
||||
"sid": self.sid,
|
||||
"_stripe_version": STRIPE_VERSION,
|
||||
"key": self.stripe_public_key,
|
||||
"payment_user_agent": f"stripe.js/{STRIPE_JS_VERSION}; stripe-js-v3/{STRIPE_JS_VERSION}; checkout",
|
||||
"client_attribution_metadata[client_session_id]": self.client_session_id,
|
||||
"client_attribution_metadata[checkout_session_id]": self.session_id,
|
||||
"client_attribution_metadata[merchant_integration_source]": "checkout",
|
||||
"client_attribution_metadata[merchant_integration_version]": "hosted_checkout",
|
||||
"client_attribution_metadata[payment_method_selection_flow]": "automatic",
|
||||
"client_attribution_metadata[checkout_config_id]": self.checkout_config_id,
|
||||
}
|
||||
|
||||
try:
|
||||
resp = self.session.post(api_url, data=data, headers=self._get_stripe_headers(), timeout=30)
|
||||
|
||||
if resp.status_code == 200:
|
||||
result = resp.json()
|
||||
payment_method_id = result.get("id", "")
|
||||
if payment_method_id:
|
||||
return payment_method_id
|
||||
|
||||
error_text = resp.text[:200] if resp.text else "无响应"
|
||||
log_progress(f"[X] 创建支付方式失败: {resp.status_code} - {error_text}")
|
||||
return ""
|
||||
except Exception as e:
|
||||
log_progress(f"[X] 创建支付方式异常: {e}")
|
||||
return ""
|
||||
|
||||
def confirm_payment(self, payment_method_id: str, captcha_token: str = "",
|
||||
rv_timestamp: str = "") -> tuple:
|
||||
"""Step 2: 确认支付
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, error_message: str)
|
||||
"""
|
||||
api_url = f"https://api.stripe.com/v1/payment_pages/{self.session_id}/confirm"
|
||||
|
||||
data = {
|
||||
"eid": "NA",
|
||||
"payment_method": payment_method_id,
|
||||
"expected_amount": "0",
|
||||
"consent[terms_of_service]": "accepted",
|
||||
"tax_id_collection[purchasing_as_business]": "false",
|
||||
"expected_payment_method_type": "sepa_debit",
|
||||
"_stripe_version": STRIPE_VERSION,
|
||||
"guid": self.guid,
|
||||
"muid": self.muid,
|
||||
"sid": self.sid,
|
||||
"key": self.stripe_public_key,
|
||||
"version": STRIPE_JS_VERSION,
|
||||
"referrer": "https://chatgpt.com",
|
||||
"client_attribution_metadata[client_session_id]": self.client_session_id,
|
||||
"client_attribution_metadata[checkout_session_id]": self.session_id,
|
||||
"client_attribution_metadata[merchant_integration_source]": "checkout",
|
||||
"client_attribution_metadata[merchant_integration_version]": "hosted_checkout",
|
||||
"client_attribution_metadata[payment_method_selection_flow]": "automatic",
|
||||
"client_attribution_metadata[checkout_config_id]": self.checkout_config_id,
|
||||
}
|
||||
|
||||
if self.init_checksum:
|
||||
data["init_checksum"] = self.init_checksum
|
||||
if self.js_checksum:
|
||||
data["js_checksum"] = self.js_checksum
|
||||
if captcha_token:
|
||||
data["passive_captcha_token"] = captcha_token
|
||||
data["passive_captcha_ekey"] = ""
|
||||
if rv_timestamp:
|
||||
data["rv_timestamp"] = rv_timestamp
|
||||
|
||||
try:
|
||||
resp = self.session.post(api_url, data=data, headers=self._get_stripe_headers(), timeout=30)
|
||||
|
||||
if resp.status_code == 200:
|
||||
result = resp.json()
|
||||
state = result.get("state", "")
|
||||
|
||||
if state in ["succeeded", "processing", "processing_subscription"]:
|
||||
return True, ""
|
||||
elif state == "failed":
|
||||
error = result.get("error", {})
|
||||
error_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
|
||||
return False, f"支付失败: {error_msg}"
|
||||
else:
|
||||
# 其他状态继续轮询
|
||||
return True, ""
|
||||
|
||||
error_text = resp.text[:200] if resp.text else "无响应"
|
||||
return False, f"确认失败: {resp.status_code} - {error_text}"
|
||||
except Exception as e:
|
||||
return False, f"确认异常: {e}"
|
||||
|
||||
def poll_payment_status(self, max_attempts: int = 20) -> tuple:
|
||||
"""Step 3: 轮询支付状态
|
||||
|
||||
Returns:
|
||||
tuple: (state: str, error_message: str)
|
||||
"""
|
||||
api_url = f"https://api.stripe.com/v1/payment_pages/{self.session_id}/poll?key={self.stripe_public_key}"
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
resp = self.session.get(api_url, headers=self._get_stripe_headers(), timeout=30)
|
||||
|
||||
if resp.status_code == 200:
|
||||
result = resp.json()
|
||||
state = result.get("state", "")
|
||||
|
||||
if state == "succeeded":
|
||||
return "succeeded", ""
|
||||
elif state in ["failed", "canceled"]:
|
||||
return state, f"支付 {state}"
|
||||
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
log_progress(f"[!] 轮询异常: {e}")
|
||||
time.sleep(2)
|
||||
|
||||
return "timeout", "轮询超时"
|
||||
|
||||
def complete_payment(self, iban: str, name: str, email: str,
|
||||
address: str, city: str, postal_code: str,
|
||||
country: str = "DE") -> tuple:
|
||||
"""执行完整支付流程
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, error_message: str)
|
||||
"""
|
||||
# 获取页面参数
|
||||
self.fetch_checkout_page()
|
||||
|
||||
# Step 1: 创建支付方式
|
||||
payment_method_id = self.create_payment_method(
|
||||
iban, name, email, address, city, postal_code, country
|
||||
)
|
||||
if not payment_method_id:
|
||||
return False, "创建支付方式失败"
|
||||
|
||||
# Step 2: 确认支付
|
||||
success, error = self.confirm_payment(payment_method_id)
|
||||
if not success:
|
||||
return False, error
|
||||
|
||||
# Step 3: 轮询状态
|
||||
state, error = self.poll_payment_status(15)
|
||||
if state == "succeeded":
|
||||
return True, ""
|
||||
|
||||
return False, error or f"支付失败: {state}"
|
||||
|
||||
|
||||
def api_payment_with_retry(checkout_url: str, email: str, session=None, proxy: str = None,
|
||||
max_retries: int = 3, get_iban_func=None,
|
||||
get_address_func=None, get_name_func=None,
|
||||
progress_callback=None) -> tuple:
|
||||
"""使用 API 完成支付流程(带 IBAN 重试)
|
||||
|
||||
Args:
|
||||
checkout_url: Stripe checkout URL
|
||||
email: 邮箱地址
|
||||
session: 可选的 curl_cffi session
|
||||
proxy: 代理地址
|
||||
max_retries: 最大重试次数
|
||||
get_iban_func: 获取 IBAN 的函数,签名: func() -> str
|
||||
get_address_func: 获取地址的函数,签名: func() -> tuple(street, postal_code, city)
|
||||
get_name_func: 获取姓名的函数,签名: func() -> str
|
||||
progress_callback: 进度回调函数
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, error_message: str)
|
||||
"""
|
||||
def log_cb(msg):
|
||||
if progress_callback:
|
||||
progress_callback(msg)
|
||||
else:
|
||||
log_progress(msg)
|
||||
|
||||
# 默认的 IBAN/地址/姓名生成函数
|
||||
if get_iban_func is None:
|
||||
def get_iban_func():
|
||||
# 从 auto_gpt_team 导入
|
||||
try:
|
||||
from auto_gpt_team import get_sepa_ibans
|
||||
ibans = get_sepa_ibans()
|
||||
return random.choice(ibans) if ibans else ""
|
||||
except:
|
||||
return ""
|
||||
|
||||
if get_address_func is None:
|
||||
def get_address_func():
|
||||
try:
|
||||
from auto_gpt_team import SEPA_ADDRESSES
|
||||
return random.choice(SEPA_ADDRESSES)
|
||||
except:
|
||||
return ("Alexanderplatz 1", "10178", "Berlin")
|
||||
|
||||
if get_name_func is None:
|
||||
def get_name_func():
|
||||
try:
|
||||
from auto_gpt_team import FIRST_NAMES, LAST_NAMES
|
||||
return f"{random.choice(FIRST_NAMES)} {random.choice(LAST_NAMES)}"
|
||||
except:
|
||||
return "Max Mustermann"
|
||||
|
||||
# 初始化 Stripe 支付处理器
|
||||
stripe_handler = StripePaymentAPI(checkout_url, session=session, proxy=proxy)
|
||||
|
||||
last_error = ""
|
||||
|
||||
for retry in range(max_retries):
|
||||
# 生成支付信息
|
||||
iban = get_iban_func()
|
||||
if not iban:
|
||||
return False, "没有可用的 IBAN"
|
||||
|
||||
street, postal_code, city = get_address_func()
|
||||
account_name = get_name_func()
|
||||
|
||||
if retry == 0:
|
||||
log_status("API支付", "SEPA 支付处理中...")
|
||||
log_cb(f"IBAN: {iban[:8]}...")
|
||||
log_cb(f"地址: {street}, {postal_code} {city}")
|
||||
log_cb(f"姓名: {account_name}")
|
||||
else:
|
||||
log_cb(f"[!] 重试 {retry}/{max_retries-1},更换 IBAN...")
|
||||
log_cb(f"新 IBAN: {iban[:8]}...")
|
||||
|
||||
success, error = stripe_handler.complete_payment(
|
||||
iban, account_name, email, street, city, postal_code, "DE"
|
||||
)
|
||||
|
||||
if success:
|
||||
log_status("API支付", "[OK] 支付成功")
|
||||
return True, ""
|
||||
|
||||
last_error = error
|
||||
|
||||
# 分析错误类型,决定是否重试
|
||||
error_lower = error.lower() if error else ""
|
||||
|
||||
# IBAN/BIC 相关错误 - 换 IBAN 重试
|
||||
if "bank_account_unusable" in error_lower or "bic" in error_lower or "iban" in error_lower:
|
||||
log_cb(f"[!] IBAN 无效: {error}")
|
||||
if retry < max_retries - 1:
|
||||
continue
|
||||
|
||||
# 可恢复错误 - 重试
|
||||
retryable_errors = ["400", "500", "timeout", "eof", "connection", "确认失败"]
|
||||
is_retryable = any(e in error_lower for e in retryable_errors)
|
||||
|
||||
if is_retryable and retry < max_retries - 1:
|
||||
log_cb(f"[!] 支付错误: {error},重试中...")
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
# 不可恢复错误或重试用尽
|
||||
break
|
||||
|
||||
log_status("API支付", f"[X] 支付失败: {last_error}")
|
||||
return False, last_error
|
||||
|
||||
|
||||
def is_stripe_api_available() -> bool:
|
||||
"""检查 Stripe API 模式是否可用"""
|
||||
return CURL_CFFI_AVAILABLE
|
||||
5090
telegram_bot.py
5090
telegram_bot.py
File diff suppressed because it is too large
Load Diff
91
uv.lock
generated
91
uv.lock
generated
@@ -36,6 +36,63 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.4"
|
||||
@@ -123,6 +180,29 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786, upload-time = "2025-03-10T09:30:28.048Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curl-cffi"
|
||||
version = "0.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/c9/0067d9a25ed4592b022d4558157fcdb6e123516083700786d38091688767/curl_cffi-0.14.0.tar.gz", hash = "sha256:5ffbc82e59f05008ec08ea432f0e535418823cda44178ee518906a54f27a5f0f", size = 162633, upload-time = "2025-12-16T03:25:07.931Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/f0/0f21e9688eaac85e705537b3a87a5588d0cefb2f09d83e83e0e8be93aa99/curl_cffi-0.14.0-cp39-abi3-macosx_14_0_arm64.whl", hash = "sha256:e35e89c6a69872f9749d6d5fda642ed4fc159619329e99d577d0104c9aad5893", size = 3087277, upload-time = "2025-12-16T03:24:49.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/a3/0419bd48fce5b145cb6a2344c6ac17efa588f5b0061f212c88e0723da026/curl_cffi-0.14.0-cp39-abi3-macosx_15_0_x86_64.whl", hash = "sha256:5945478cd28ad7dfb5c54473bcfb6743ee1d66554d57951fdf8fc0e7d8cf4e45", size = 5804650, upload-time = "2025-12-16T03:24:51.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/07/a238dd062b7841b8caa2fa8a359eb997147ff3161288f0dd46654d898b4d/curl_cffi-0.14.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c42e8fa3c667db9ccd2e696ee47adcd3cd5b0838d7282f3fc45f6c0ef3cfdfa7", size = 8231918, upload-time = "2025-12-16T03:24:52.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/d2/ce907c9b37b5caf76ac08db40cc4ce3d9f94c5500db68a195af3513eacbc/curl_cffi-0.14.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:060fe2c99c41d3cb7f894de318ddf4b0301b08dca70453d769bd4e74b36b8483", size = 8654624, upload-time = "2025-12-16T03:24:54.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/ae/6256995b18c75e6ef76b30753a5109e786813aa79088b27c8eabb1ef85c9/curl_cffi-0.14.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b158c41a25388690dd0d40b5bc38d1e0f512135f17fdb8029868cbc1993d2e5b", size = 8010654, upload-time = "2025-12-16T03:24:56.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/10/ff64249e516b103cb762e0a9dca3ee0f04cf25e2a1d5d9838e0f1273d071/curl_cffi-0.14.0-cp39-abi3-manylinux_2_28_i686.whl", hash = "sha256:1439fbef3500fb723333c826adf0efb0e2e5065a703fb5eccce637a2250db34a", size = 7781969, upload-time = "2025-12-16T03:24:57.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/76/d6f7bb76c2d12811aa7ff16f5e17b678abdd1b357b9a8ac56310ceccabd5/curl_cffi-0.14.0-cp39-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e7176f2c2d22b542e3cf261072a81deb018cfa7688930f95dddef215caddb469", size = 7969133, upload-time = "2025-12-16T03:24:59.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/7c/cca39c0ed4e1772613d3cba13091c0e9d3b89365e84b9bf9838259a3cd8f/curl_cffi-0.14.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:03f21ade2d72978c2bb8670e9b6de5260e2755092b02d94b70b906813662998d", size = 9080167, upload-time = "2025-12-16T03:25:00.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/03/a942d7119d3e8911094d157598ae0169b1c6ca1bd3f27d7991b279bcc45b/curl_cffi-0.14.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:58ebf02de64ee5c95613209ddacb014c2d2f86298d7080c0a1c12ed876ee0690", size = 9520464, upload-time = "2025-12-16T03:25:02.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/77/78900e9b0833066d2274bda75cba426fdb4cef7fbf6a4f6a6ca447607bec/curl_cffi-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:6e503f9a103f6ae7acfb3890c843b53ec030785a22ae7682a22cc43afb94123e", size = 1677416, upload-time = "2025-12-16T03:25:04.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/7c/d2ba86b0b3e1e2830bd94163d047de122c69a8df03c5c7c36326c456ad82/curl_cffi-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:2eed50a969201605c863c4c31269dfc3e0da52916086ac54553cfa353022425c", size = 1425067, upload-time = "2025-12-16T03:25:06.454Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "datarecorder"
|
||||
version = "3.6.2"
|
||||
@@ -337,6 +417,7 @@ name = "oai-team-auto-provisioner"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "curl-cffi" },
|
||||
{ name = "drissionpage" },
|
||||
{ name = "python-telegram-bot", extra = ["job-queue"] },
|
||||
{ name = "requests" },
|
||||
@@ -348,6 +429,7 @@ dependencies = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "curl-cffi", specifier = ">=0.14.0" },
|
||||
{ name = "drissionpage", specifier = ">=4.1.1.2" },
|
||||
{ name = "python-telegram-bot", extras = ["job-queue"], specifier = ">=22.5" },
|
||||
{ name = "requests", specifier = ">=2.32.5" },
|
||||
@@ -395,6 +477,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
|
||||
Reference in New Issue
Block a user