Compare commits

60 Commits
ma ... main

Author SHA1 Message Date
85c270f55f refactor: enhance proxy pool logging with masked URLs and decrease proxy test concurrency from 20 to 10. 2026-02-11 02:22:18 +08:00
660d43161d feat: Enhance proxy testing to return detailed results including latency and errors, and display a comprehensive, formatted report in the Telegram bot. 2026-02-11 02:18:24 +08:00
be8dd745fb feat: Implement a proxy pool with concurrent testing and integrate proxy management commands into the Telegram bot. 2026-02-11 02:10:11 +08:00
2ff52d5d73 feat: Persist the scheduler's enabled state to config.toml and preserve its in-memory status during config reloads. 2026-02-11 01:40:59 +08:00
2c875594a6 feat: Add commands and help text for timed scheduler management and status. 2026-02-10 02:43:23 +08:00
4d5fa36183 feat: add configurable timed scheduler to the Telegram bot for automated tasks. 2026-02-10 02:38:45 +08:00
b9dd421714 feat: automatically clean team and tracker files after 'run all' or 'resume' tasks and notify administrators. 2026-02-09 18:02:50 +08:00
4c40949696 fix: Prevent double-prefixing of S2A team names and refactor Telegram bot's thread pool usage for non-blocking execution. 2026-02-08 02:21:57 +08:00
cb1fb57b53 feat: Prefix team account names with 'team-' in service functions and add 'CodexAuth' to gitignore. 2026-02-08 01:27:56 +08:00
713564fc25 team-mail 2026-02-07 02:42:05 +08:00
6b41c9bccd feat: Initialize CodexAuth project, including virtual environment, dependencies, and initial authentication scripts. 2026-02-07 02:22:01 +08:00
71efc3b04c feat: Install Python dependencies into virtual environment and add get_code.py script. 2026-02-07 02:03:16 +08:00
36bd799c8f fix(telegram_bot): Correct GPTMail API endpoint and authentication method
- Change API endpoint from `/api/mail/list` to `/api/generate-email`
- Update HTTP method from POST to GET request
- Replace `Authorization` header with `X-API-Key` for proper authentication
- Remove unnecessary payload parameter from API request
- Enhance response handling to extract and display generated test email
- Add error message parsing from API response for better debugging
- Improve test connection feedback to show actual generated email address
2026-02-04 03:09:28 +08:00
a7867ae406 feat(s2a_service): Add pure API authorization mode without browser
- Add S2A_API_MODE configuration option to enable browser-less authorization
- Implement S2AApiAuthorizer class using curl_cffi for browser fingerprint simulation
- Add Sentinel PoW (Proof of Work) solver with FNV-1a hashing algorithm
- Implement OAuth flow via direct API calls instead of browser automation
- Add s2a_api_authorize() function to handle email/password authentication
- Support proxy configuration for API requests
- Add requirements token generation for API authentication
- Update browser_automation.py to check S2A_API_MODE and route to API or browser flow
- Update config.py to load S2A_API_MODE from configuration
- Add api_mode option to config.toml.example with documentation
- Improves performance and stability by eliminating browser overhead while maintaining compatibility with existing browser-based flow
2026-02-02 09:26:57 +08:00
ae86ca42df feat(email_service): Support dynamic email configuration reloading
- Import EMAIL_API_AUTH and EMAIL_ROLE from config module on each function call
- Update create_email_user() to use dynamically loaded current_auth and current_role
- Update get_verification_code() to use dynamically loaded current_auth
- Update fetch_email_content() to use dynamically loaded current_auth
- Enable runtime configuration switching without service restart
2026-01-30 16:15:17 +08:00
b50ad199de feat(email_service): Add dynamic email provider configuration support
- Import EMAIL_PROVIDER from config at runtime in unified functions to enable dynamic provider switching
- Update unified_generate_email() to fetch current provider configuration on each call
- Update unified_create_email() to fetch current provider configuration on each call
- Update unified_get_verification_code() to fetch current provider configuration on each call
- Update unified_fetch_emails() to fetch current provider configuration on each call
- Update fallback comments to reflect support for both KYX and Cloud Mail systems
- Allows email provider to be switched without restarting the application
2026-01-30 16:02:00 +08:00
cb55db7901 feat(config,email_service): Add Cloud Mail API path auto-completion utility
- Add get_cloudmail_api_base() helper function to automatically append /api/public path to EMAIL_API_BASE
- Update create_email_user() to use get_cloudmail_api_base() for consistent API endpoint construction
- Update get_verification_code() to use get_cloudmail_api_base() for email list retrieval
- Update fetch_email_content() to use get_cloudmail_api_base() for email list retrieval
- Refactor telegram_bot.py to use centralized path completion logic instead of inline implementation
- Improve API endpoint consistency across email service operations and reduce code duplication
2026-01-30 15:43:48 +08:00
1c17015669 refactor(telegram_bot): Improve Cloud Mail domain management and API response handling
- Extract message object using get_message() helper in _cloudmail_add_domain and _cloudmail_del_domain methods for consistency
- Replace direct update.message.reply_text() calls with message.reply_text() throughout domain management functions
- Add comprehensive HTTP status code validation in API connection check
- Implement empty response detection to handle edge cases where API returns blank content
- Add JSON parsing error handling with fallback error message for non-JSON responses
- Improve error messaging to include HTTP status codes and API error codes for better debugging
- Enhance robustness of email service API validation with multiple safety checks
2026-01-30 15:30:16 +08:00
f39dff8ee6 feat(telegram_bot): Add Cloud Mail configuration and batch team processing
- Add Cloud Mail API configuration support (api_base, api_auth, domains) in config.py
- Implement run_teams_by_count() function for processing specified number of teams with smart filtering
- Add Cloud Mail management commands: /cloudmail, /cloudmail_token, /cloudmail_api, /cloudmail_domains
- Add callback handlers for run_all, run, and cloudmail interactive operations
- Refactor /run command to use interactive selection for count and email service instead of direct argument
- Update bot command descriptions and help text to reflect new functionality
- Add Cloud Mail domain management and token configuration capabilities
- Enable batch processing with progress tracking and automatic team completion detection
2026-01-30 14:07:37 +08:00
79c3eb733c feat(registration): Add email retry mechanism for API registration failures
- Add `allow_fallback` parameter to `register_openai_account_auto()` to control fallback behavior
- Return `"retry_new_email"` status when API registration fails without fallback enabled
- Return `"domain_blacklisted"` status when domain is blacklisted during registration
- Update `register_only()` to handle and propagate `"retry_new_email"` status
- Update `register_and_authorize()` to handle and propagate `"retry_new_email"` status
- Add retry logic in `process_accounts()` to regenerate email and reinvite on API failures
- Add retry logic in `_process_single_account_worker()` to regenerate email and reinvite on API failures
- Improve error handling to distinguish between domain blacklist and API failures requiring email regeneration
2026-01-30 12:24:40 +08:00
e4f330500d feat(telegram_bot): Add concurrent team registration with multi-worker support
- Implement concurrent registration processing with configurable worker count
- Add CONCURRENT_ENABLED and CONCURRENT_WORKERS config options for parallel execution
- Replace single-threaded loop with task queue and worker thread pool architecture
- Add per-worker step tracking and status display in progress messages
- Implement thread-safe result collection with results_lock for concurrent access
- Update progress UI to show individual worker status and concurrent worker count
- Refactor step callback to support multiple workers with worker_id tracking
- Add graceful shutdown handling for concurrent workers
- Improve progress message updates to only refresh when content changes
- Optimize performance by allowing multiple registrations to run in parallel
2026-01-30 10:50:25 +08:00
85949d8ede 2 2026-01-30 10:28:16 +08:00
fcf1354bc7 1 2026-01-30 10:20:28 +08:00
6d3aa84af9 feat(telegram_bot): Add simplified team registration format with output options
- Add support for simplified team_reg callback format (team_reg:1, team_reg:3, team_reg:5)
- Implement direct count parsing from callback data without intermediate steps
- Add output method selection UI with JSON file and team.json options
- Add custom count input handler for flexible registration quantities (1-50)
- Improve user experience by reducing callback navigation steps
- Set user state flag for awaiting custom count input
2026-01-30 10:15:26 +08:00
11395bf1ba feat(payment): Integrate Stripe API and refactor payment flow
- Add new stripe_api.py module with StripePaymentAPI class and payment retry logic
- Import Stripe payment module in auto_gpt_team.py with graceful fallback handling
- Refactor run_payment_flow() to extract form filling logic into _fill_payment_form()
- Simplify error handling by returning structured tuples (success, error_type, error_msg)
- Remove redundant comments and streamline selector logic for payment form elements
- Improve code maintainability by separating concerns between form filling and flow orchestration
- Add STRIPE_API_AVAILABLE flag to track payment module availability at runtime
2026-01-30 09:57:55 +08:00
ad03fab8e9 1 2026-01-30 09:05:59 +08:00
b7e3cd840b feat(telegram_bot): Add update_token command for mail API token management
- Add new /update_token command handler to update mail API token in config.toml
- Register command in handlers list and set_my_commands for bot menu
- Add command documentation to help text with usage example
- Implement token update logic with TOML file read/write operations
- Display masked token values (first 8 and last 4 characters) for security
- Add automatic config reload after token update
- Include error handling for missing tomli_w dependency and file operations
- Restrict command to admin users only via @admin_only decorator
2026-01-30 08:57:50 +08:00
20e2719d0e feat(telegram_bot): Add verify_all command for force account re-verification
- Add new /verify_all command to force re-verify all accounts with tokens
- Update command registration to include verify_all handler
- Modify _validate_and_cleanup_accounts() to accept force_all parameter
- Add progress bar visualization using block characters (█░)
- Implement progress bar helper function with percentage display
- Update verification logic to handle two modes: normal and force-all
- Enhance user messages with token count and mode-specific text
- Update help text and command descriptions in Chinese
- Improve progress tracking with visual feedback during verification
- Separate /verify (unverified only) from /verify_all (all with tokens)
2026-01-30 08:53:40 +08:00
75a0dccebe feat(registration): Add email retry mechanism with team invitation support
- Add automatic email retry logic when verification code times out (5-second timeout)
- Implement new email creation and team invitation for failed verification attempts
- Add max_email_retries parameter to control retry attempts (default: 3)
- Add team_name parameter to enable automatic team invitations for new emails
- Return special "new_email:xxx@xxx.com:password" format when new email is used
- Update register_openai_account_auto() to support team_name parameter
- Update register_and_authorize() to support team_name parameter and return new email info
- Improve verification code timeout handling with configurable retry intervals
- Add nonlocal verification_timeout flag to track timeout state across retries
- Update docstrings to document new parameters and return value changes
2026-01-30 08:46:03 +08:00
b7e658c567 4 2026-01-30 06:10:31 +08:00
e43bd390f0 3 2026-01-28 06:59:22 +08:00
eb255fdf77 1 2026-01-28 06:54:50 +08:00
8d5f8fe3bb 9 2026-01-27 10:34:25 +08:00
52b875a7f9 8 2026-01-27 10:26:21 +08:00
a4f542ace2 7 2026-01-27 10:21:02 +08:00
ad10d1f2b7 6 2026-01-27 10:08:10 +08:00
06eaff03b9 5 2026-01-27 09:59:16 +08:00
a973343b48 4 2026-01-27 09:47:12 +08:00
c937bc7356 3 2026-01-27 09:30:57 +08:00
e14aabd0e2 2 2026-01-27 09:25:04 +08:00
935531955f 1 2026-01-27 09:14:49 +08:00
6cafaa4ab7 多线程 2026-01-27 09:08:34 +08:00
8cb7a50bb9 5 2026-01-26 07:17:55 +08:00
adb60cdfd6 4 2026-01-26 06:58:04 +08:00
20cdf8060d chore: update auto_gpt_team.py 2026-01-25 06:38:15 +08:00
6364d43c90 4 2026-01-25 06:16:01 +08:00
86206f8a97 3 2026-01-25 06:12:20 +08:00
c2aa9785cb 2 2026-01-25 06:10:05 +08:00
ccff201fde 1 2026-01-25 06:00:24 +08:00
32e926c4af 协议 2026-01-25 05:40:08 +08:00
af161cca4f a 2026-01-24 08:06:56 +08:00
970340fbd4 a 2026-01-24 07:54:40 +08:00
effc1add37 feat(email_domains): Add domain validation and improve domain management
- Add validate_domain_format() function with comprehensive domain format validation
* Validates domain structure (must contain dot, valid characters, proper TLD length)
* Normalizes domain format (adds @ prefix, removes quotes/special chars)
* Returns validation status with detailed error messages
- Update add_email_domains() to use new validation function
* Track invalid domains separately in return tuple
* Return (added, skipped, invalid, total) instead of (added, skipped, total)
* Improve error handling and domain normalization
- Add get_file_domains_count() function to retrieve txt file domain count
- Update clear_email_domains() to return count of cleared domains
- Enhance telegram_bot.py command menu organization
* Add s2a command handler and callback for S2A service management panel
* Reorganize bot commands with category comments (基础信息, 任务控制, 配置管理, etc.)
* Add missing commands: clean_errors, clean_teams, iban_list, iban_add, iban_clear, domain_list, domain_add, domain_del, domain_clear, team_fingerprint, team_register, s2a
- Update domain_add command help text with format requirements
- Improve code documentation and consistency across both files
2026-01-24 07:52:10 +08:00
6b914bad41 a 2026-01-24 07:36:18 +08:00
d93383fe23 a 2026-01-24 07:22:10 +08:00
c6ab6b3123 u 2026-01-24 07:09:54 +08:00
fb3ebae995 u 2026-01-24 06:57:25 +08:00
289e8ec71f update 2026-01-24 06:51:28 +08:00
0e8b5ba237 feat(telegram_bot): Add AutoGPTPlus management panel with configuration controls
- Add /autogptplus command with interactive menu for ChatGPT subscription automation
- Implement callback handler for AutoGPTPlus actions (config view, token setup, email/API testing)
- Add _show_autogptplus_config() to display current configuration with masked sensitive data
- Add _prompt_autogptplus_token() to handle Cloud Mail API token input and updates
- Add _test_autogptplus_email() and _test_autogptplus_api() for testing functionality
- Add _update_autogptplus_token() to persist token changes to config.toml
- Register AutoGPTPlus command in bot command list with Chinese description
- Update help text to include AutoGPTPlus management panel section
- Add admin-only access control for all AutoGPTPlus operations
- Provides centralized management interface for email domains, SEPA IBANs, and API configuration
2026-01-24 06:35:29 +08:00
7c4688895e update 2026-01-24 06:07:31 +08:00
16 changed files with 11557 additions and 457 deletions

6
.gitignore vendored
View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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:

View File

@@ -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 = ""

View File

@@ -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)

0
proxy.txt Normal file
View File

354
proxy_pool.py Normal file
View 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()

View File

@@ -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",

View File

@@ -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
View File

@@ -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":
@@ -420,6 +453,29 @@ def process_accounts(accounts: list, team_name: str, team_index: int = 0,
log.error("无法创建有效的新邮箱")
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")
@@ -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

View File

@@ -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"canSharefunction 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 (无需选择 workspaceS2A 授权链接已包含)
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
View 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

File diff suppressed because it is too large Load Diff

91
uv.lock generated
View File

@@ -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"