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 nul
.claude/settings.local.json .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 _cfg, _raw_teams, TEAMS
global EMAIL_PROVIDER, INCLUDE_TEAM_OWNERS, AUTH_PROVIDER 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 BROWSER_HEADLESS, ACCOUNTS_PER_TEAM
global GPTMAIL_API_KEYS, GPTMAIL_DOMAINS, GPTMAIL_PREFIX global GPTMAIL_API_KEYS, GPTMAIL_DOMAINS, GPTMAIL_PREFIX
global PROXY_ENABLED, PROXIES global PROXY_ENABLED, PROXIES
global S2A_API_BASE, S2A_ADMIN_KEY, S2A_ADMIN_TOKEN 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 = { result = {
"success": True, "success": True,
@@ -349,12 +354,24 @@ def reload_config() -> dict:
_account = _cfg.get("account", {}) _account = _cfg.get("account", {})
ACCOUNTS_PER_TEAM = _account.get("accounts_per_team", 4) 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 配置
_gptmail = _cfg.get("gptmail", {}) _gptmail = _cfg.get("gptmail", {})
GPTMAIL_PREFIX = _gptmail.get("prefix", "") GPTMAIL_PREFIX = _gptmail.get("prefix", "")
GPTMAIL_DOMAINS = _gptmail.get("domains", []) GPTMAIL_DOMAINS = _gptmail.get("domains", [])
GPTMAIL_API_KEYS = _gptmail.get("api_keys", []) or ["gpt-test"] 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_top = _cfg.get("proxy_enabled")
_proxy_enabled_browser = _cfg.get("browser", {}).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_PRIORITY = _s2a.get("priority", 50)
S2A_GROUP_NAMES = _s2a.get("group_names", []) S2A_GROUP_NAMES = _s2a.get("group_names", [])
S2A_GROUP_IDS = _s2a.get("group_ids", []) 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: except Exception as e:
errors.append(f"config.toml: {e}") errors.append(f"config.toml: {e}")
@@ -404,7 +432,7 @@ def reload_config() -> dict:
# 邮箱系统选择 # 邮箱系统选择
EMAIL_PROVIDER = _cfg.get("email_provider", "kyx") # "kyx" 或 "gptmail" EMAIL_PROVIDER = _cfg.get("email_provider", "kyx") # "kyx" 或 "gptmail"
# 原有邮箱系统 (KYX) # 原有邮箱系统 (KYX / Cloud Mail)
_email = _cfg.get("email", {}) _email = _cfg.get("email", {})
EMAIL_API_BASE = _email.get("api_base", "") EMAIL_API_BASE = _email.get("api_base", "")
EMAIL_API_AUTH = _email.get("api_auth", "") 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_ROLE = _email.get("role", "gpt-team")
EMAIL_WEB_URL = _email.get("web_url", "") 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 临时邮箱配置
_gptmail = _cfg.get("gptmail", {}) _gptmail = _cfg.get("gptmail", {})
GPTMAIL_API_BASE = _gptmail.get("api_base", "https://mail.chatgpt.org.uk") 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_PRIORITY = _s2a.get("priority", 50)
S2A_GROUP_NAMES = _s2a.get("group_names", []) S2A_GROUP_NAMES = _s2a.get("group_names", [])
S2A_GROUP_IDS = _s2a.get("group_ids", []) S2A_GROUP_IDS = _s2a.get("group_ids", [])
S2A_API_MODE = _s2a.get("api_mode", False) # 是否使用纯 API 授权模式 (无需浏览器)
# 账号 # 账号
_account = _cfg.get("account", {}) _account = _cfg.get("account", {})
DEFAULT_PASSWORD = _account.get("default_password", "kfcvivo50") DEFAULT_PASSWORD = _account.get("default_password", "kfcvivo50")
ACCOUNTS_PER_TEAM = _account.get("accounts_per_team", 4) 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", {}) _reg = _cfg.get("register", {})
REGISTER_NAME = _reg.get("name", "test") 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_CHECK_INTERVAL = _telegram.get("check_interval", 3600) # 默认1小时检查一次
TELEGRAM_LOW_STOCK_THRESHOLD = _telegram.get("low_stock_threshold", 10) # 低库存阈值 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 和 proxies 可能在顶层或被误放在 browser section 下
_proxy_enabled_top = _cfg.get("proxy_enabled") _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: def generate_random_email(prefix_len: int = 8) -> str:
prefix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=prefix_len)) 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: def generate_email_for_user(username: str) -> str:
safe = re.sub(r'[^a-zA-Z0-9]', '', username.lower())[:20] 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: def get_team(index: int = 0) -> dict:

View File

@@ -154,6 +154,11 @@ priority = 50
group_ids = [] group_ids = []
# 分组名称列表 (优先使用 group_ids如果未配置则通过名称查询 ID) # 分组名称列表 (优先使用 group_ids如果未配置则通过名称查询 ID)
group_names = [] group_names = []
# 是否使用纯 API 授权模式 (无需浏览器)
# 开启后使用 curl_cffi 直接调用 OpenAI 认证 API 完成授权
# 优点: 更快、更稳定、无需浏览器
# 缺点: 需要安装 curl_cffi (pip install curl_cffi)
api_mode = false
# ==================== 账号配置 ==================== # ==================== 账号配置 ====================
[account] [account]
@@ -162,6 +167,15 @@ default_password = "YourSecurePassword@2025"
# 每个 Team 下创建的账号数量 # 每个 Team 下创建的账号数量
accounts_per_team = 4 accounts_per_team = 4
# ==================== 并发处理配置 ====================
# 启用后可同时处理多个账号,大幅提升效率
[concurrent]
# 是否启用并发处理 (默认关闭)
enabled = false
# 并发数量 (同时运行的浏览器实例数)
# 建议根据机器配置设置: 4核8G内存建议设置为 2-4
workers = 4
# ==================== 注册配置 ==================== # ==================== 注册配置 ====================
[register] [register]
# 注册时使用的用户名 (实际会使用随机生成的英文名) # 注册时使用的用户名 (实际会使用随机生成的英文名)
@@ -220,3 +234,44 @@ notify_on_error = true
check_interval = 3600 check_interval = 3600
# 低库存预警阈值 (正常账号数低于此值时预警) # 低库存预警阈值 (正常账号数低于此值时预警)
low_stock_threshold = 10 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_random_gptmail_domain,
get_next_gptmail_key, get_next_gptmail_key,
get_gptmail_keys, get_gptmail_keys,
get_cloudmail_api_base,
) )
from logger import log from logger import log
@@ -425,7 +426,7 @@ def generate_random_email() -> str:
"""生成随机邮箱地址: {random_str}oaiteam@{random_domain}""" """生成随机邮箱地址: {random_str}oaiteam@{random_domain}"""
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
domain = get_random_domain() domain = get_random_domain()
email = f"{random_str}oaiteam@{domain}" email = f"team-{random_str}oaiteam@{domain}"
log.success(f"生成邮箱: {email}") log.success(f"生成邮箱: {email}")
return email return email
@@ -441,14 +442,17 @@ def create_email_user(email: str, password: str = None, role_name: str = None) -
Returns: Returns:
tuple: (success, message) tuple: (success, message)
""" """
# 每次调用时重新获取配置,支持动态切换
from config import EMAIL_API_AUTH as current_auth, EMAIL_ROLE as current_role
if password is None: if password is None:
password = DEFAULT_PASSWORD password = DEFAULT_PASSWORD
if role_name is None: 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 = { headers = {
"Authorization": EMAIL_API_AUTH, "Authorization": current_auth,
"Content-Type": "application/json" "Content-Type": "application/json"
} }
payload = { payload = {
@@ -484,9 +488,12 @@ def get_verification_code(email: str, max_retries: int = None, interval: int = N
Returns: Returns:
tuple: (code, error, email_time) - 验证码、错误信息、邮件时间 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 = { headers = {
"Authorization": EMAIL_API_AUTH, "Authorization": current_auth,
"Content-Type": "application/json" "Content-Type": "application/json"
} }
payload = {"toEmail": email} payload = {"toEmail": email}
@@ -565,9 +572,12 @@ def fetch_email_content(email: str) -> list:
Returns: Returns:
list: 邮件列表 list: 邮件列表
""" """
url = f"{EMAIL_API_BASE}/emailList" # 每次调用时重新获取配置,支持动态切换
from config import EMAIL_API_AUTH as current_auth
url = f"{get_cloudmail_api_base()}/emailList"
headers = { headers = {
"Authorization": EMAIL_API_AUTH, "Authorization": current_auth,
"Content-Type": "application/json" "Content-Type": "application/json"
} }
payload = {"toEmail": email} payload = {"toEmail": email}
@@ -631,17 +641,20 @@ def unified_generate_email() -> str:
Returns: Returns:
str: 邮箱地址 str: 邮箱地址
""" """
if EMAIL_PROVIDER == "gptmail": # 每次调用时重新获取配置,支持动态切换
from config import EMAIL_PROVIDER as current_provider
if current_provider == "gptmail":
# 生成随机前缀 + oaiteam 后缀,确保不重复 # 生成随机前缀 + oaiteam 后缀,确保不重复
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) 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 domain = get_random_gptmail_domain() or None
email, error = gptmail_service.generate_email(prefix=prefix, domain=domain) email, error = gptmail_service.generate_email(prefix=prefix, domain=domain)
if email: if email:
return email return email
log.warning(f"GPTMail 生成失败,回退到 KYX: {error}") log.warning(f"GPTMail 生成失败,回退到 KYX: {error}")
# 默认使用 KYX 系统 # 默认使用 KYX / Cloud Mail 系统
return generate_random_email() return generate_random_email()
@@ -651,10 +664,13 @@ def unified_create_email() -> tuple[str, str]:
Returns: Returns:
tuple: (email, password) tuple: (email, password)
""" """
if EMAIL_PROVIDER == "gptmail": # 每次调用时重新获取配置,支持动态切换
from config import EMAIL_PROVIDER as current_provider
if current_provider == "gptmail":
# 生成随机前缀 + oaiteam 后缀,确保不重复 # 生成随机前缀 + oaiteam 后缀,确保不重复
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) 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 domain = get_random_gptmail_domain() or None
email, error = gptmail_service.generate_email(prefix=prefix, domain=domain) email, error = gptmail_service.generate_email(prefix=prefix, domain=domain)
if email: if email:
@@ -662,7 +678,7 @@ def unified_create_email() -> tuple[str, str]:
return email, DEFAULT_PASSWORD return email, DEFAULT_PASSWORD
log.warning(f"GPTMail 生成失败,回退到 KYX: {error}") log.warning(f"GPTMail 生成失败,回退到 KYX: {error}")
# 默认使用 KYX 系统 # 默认使用 KYX / Cloud Mail 系统
email = generate_random_email() email = generate_random_email()
success, msg = create_email_user(email, DEFAULT_PASSWORD) success, msg = create_email_user(email, DEFAULT_PASSWORD)
if success or "已存在" in msg: if success or "已存在" in msg:
@@ -681,10 +697,13 @@ def unified_get_verification_code(email: str, max_retries: int = None, interval:
Returns: Returns:
tuple: (code, error, email_time) - 验证码、错误信息、邮件时间 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) return gptmail_service.get_verification_code(email, max_retries, interval)
# 默认使用 KYX 系统 # 默认使用 KYX / Cloud Mail 系统
return get_verification_code(email, max_retries, interval) return get_verification_code(email, max_retries, interval)
@@ -697,9 +716,12 @@ def unified_fetch_emails(email: str) -> list:
Returns: Returns:
list: 邮件列表 list: 邮件列表
""" """
if EMAIL_PROVIDER == "gptmail": # 每次调用时重新获取配置,支持动态切换
from config import EMAIL_PROVIDER as current_provider
if current_provider == "gptmail":
emails, error = gptmail_service.get_emails(email) emails, error = gptmail_service.get_emails(email)
return emails return emails
# 默认使用 KYX 系统 # 默认使用 KYX / Cloud Mail 系统
return fetch_email_content(email) 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" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"curl-cffi>=0.14.0",
"drissionpage>=4.1.1.2", "drissionpage>=4.1.1.2",
"python-telegram-bot[job-queue]>=22.5", "python-telegram-bot[job-queue]>=22.5",
"requests>=2.32.5", "requests>=2.32.5",

View File

@@ -4,3 +4,6 @@ requests>=2.32.5
rich>=14.2.0 rich>=14.2.0
setuptools>=80.9.0 setuptools>=80.9.0
tomli>=2.3.0 tomli>=2.3.0
# 协议模式依赖 (可选,用于 API 快速注册)
curl_cffi>=0.7.0

772
run.py
View File

@@ -5,9 +5,9 @@
# 1. 检查未完成账号 (自动恢复) # 1. 检查未完成账号 (自动恢复)
# 2. 批量创建邮箱 (4个) # 2. 批量创建邮箱 (4个)
# 3. 一次性邀请到 Team # 3. 一次性邀请到 Team
# 4. 逐个注册 OpenAI 账号 # 4. 逐个注册 OpenAI 账号 (或并发处理)
# 5. 逐个 Codex 授权 # 5. 逐个 Codex 授权
# 6. 逐个添加到 CRS # 6. 逐个添加到 CRS/S2A
# 7. 切换下一个 Team # 7. 切换下一个 Team
import time import time
@@ -15,18 +15,21 @@ import random
import signal import signal
import sys import sys
import atexit import atexit
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from config import ( from config import (
TEAMS, ACCOUNTS_PER_TEAM, DEFAULT_PASSWORD, AUTH_PROVIDER, TEAMS, ACCOUNTS_PER_TEAM, DEFAULT_PASSWORD, AUTH_PROVIDER,
add_domain_to_blacklist, get_domain_from_email, is_email_blacklisted, 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 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 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 crs_service import crs_add_account, crs_sync_team_owners, crs_verify_token
from cpa_service import cpa_verify_connection from cpa_service import cpa_verify_connection
from s2a_service import s2a_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 ( from utils import (
save_to_csv, save_to_csv,
load_team_tracker, load_team_tracker,
@@ -58,6 +61,8 @@ except ImportError:
_tracker = None _tracker = None
_current_results = [] _current_results = []
_shutdown_requested = False _shutdown_requested = False
_tracker_lock = threading.Lock() # 用于并发时保护 tracker 操作
_auth_callback_lock = threading.Lock() # 授权回调锁 - 确保同一时间只有一个线程进行授权回调
def _save_state(): def _save_state():
@@ -128,7 +133,7 @@ def process_single_team(team: dict, team_index: int = 0, teams_total: int = 0) -
# 如果普通成员已完成目标数量,且没有未完成的 Owner跳过 # 如果普通成员已完成目标数量,且没有未完成的 Owner跳过
owner_incomplete = len(owner_accounts) owner_incomplete = len(owner_accounts)
if member_count >= ACCOUNTS_PER_TEAM and completed_count == member_count and owner_incomplete == 0: 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} 个成员账号,跳过") log.success(f"{team_name} 已完成 {completed_count}/{ACCOUNTS_PER_TEAM} 个成员账号,跳过")
return results, [] return results, []
@@ -224,12 +229,21 @@ def process_single_team(team: dict, team_index: int = 0, teams_total: int = 0) -
# ========== 阶段 3: 处理所有账号 (注册 + Codex 授权 + 入库) ========== # ========== 阶段 3: 处理所有账号 (注册 + Codex 授权 + 入库) ==========
if all_to_process: if all_to_process:
log.section(f"阶段 3: 逐个注册 OpenAI + Codex 授权 + 入库") # 根据配置选择处理模式
all_results = process_accounts( if CONCURRENT_ENABLED and len(all_to_process) > 1:
all_to_process, team_name, log.section(f"阶段 3: 并发处理 {len(all_to_process)} 个账号 (并发数: {min(CONCURRENT_WORKERS, len(all_to_process))})")
team_index=team_index, teams_total=teams_total, all_results = process_accounts_concurrent(
include_owner=include_owner 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) results.extend(all_results)
# ========== Team 处理完成 ========== # ========== Team 处理完成 ==========
@@ -393,7 +407,26 @@ def process_accounts(accounts: list, team_name: str, team_index: int = 0,
else: else:
# 新账号: 注册 + Codex 授权 # 新账号: 注册 + Codex 授权
progress_update(phase="注册", step="注册 OpenAI...") 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": if register_success == "domain_blacklisted":
@@ -420,6 +453,29 @@ def process_accounts(accounts: list, team_name: str, team_index: int = 0,
log.error("无法创建有效的新邮箱") log.error("无法创建有效的新邮箱")
continue # 跳过当前账号,继续下一个 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": if register_success and register_success != "domain_blacklisted":
update_account_status(_tracker, team_name, email, "registered") 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: 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") log.info(f"等待 {wait_time}s 后处理下一个账号...", icon="wait")
time.sleep(wait_time) time.sleep(wait_time)
return results 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(): def _print_system_config():
"""打印当前系统配置""" """打印当前系统配置"""
from config import ( from config import (
EMAIL_PROVIDER, AUTH_PROVIDER, ACCOUNTS_PER_TEAM, EMAIL_PROVIDER, AUTH_PROVIDER, ACCOUNTS_PER_TEAM,
INCLUDE_TEAM_OWNERS, BROWSER_RANDOM_FINGERPRINT, INCLUDE_TEAM_OWNERS, BROWSER_RANDOM_FINGERPRINT,
S2A_API_BASE, CPA_API_BASE, CRS_API_BASE, S2A_API_BASE, CPA_API_BASE, CRS_API_BASE,
PROXY_ENABLED, PROXIES PROXY_ENABLED, PROXIES,
CONCURRENT_ENABLED, CONCURRENT_WORKERS
) )
log.section("系统配置") log.section("系统配置")
@@ -560,6 +1174,12 @@ def _print_system_config():
log.info(f"Owner 入库: {'✓ 开启' if INCLUDE_TEAM_OWNERS else '✗ 关闭'}", icon="config") log.info(f"Owner 入库: {'✓ 开启' if INCLUDE_TEAM_OWNERS else '✗ 关闭'}", icon="config")
log.info(f"随机指纹: {'✓ 开启' if BROWSER_RANDOM_FINGERPRINT 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: if PROXY_ENABLED and PROXIES:
log.info(f"代理: 已启用 ({len(PROXIES)} 个)", icon="proxy") log.info(f"代理: 已启用 ({len(PROXIES)} 个)", icon="proxy")
else: else:
@@ -591,26 +1211,59 @@ def run_all_teams():
log.warning(f"发现 {total_incomplete} 个未完成账号,将优先处理") log.warning(f"发现 {total_incomplete} 个未完成账号,将优先处理")
_current_results = [] _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("全部流程"): with Timer("全部流程"):
# ========== 处理所有 Team (成员 + Owner 一起) ========== # ========== 处理需要处理的 Team (成员 + Owner 一起) ==========
for i, team in enumerate(TEAMS): for idx, (original_idx, team) in enumerate(teams_to_process):
if _shutdown_requested: if _shutdown_requested:
log.warning("检测到中断请求,停止处理...") log.warning("检测到中断请求,停止处理...")
break break
log.separator("", 60) log.separator("", 60)
team_email = team.get('account') or team.get('owner_email', '') 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) log.separator("", 60)
# 传递 Team 序号信息 # 传递 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) _current_results.extend(results)
# Team 之间的间隔 # Team 之间的间隔
if i < teams_total - 1 and not _shutdown_requested: if idx < teams_total - 1 and not _shutdown_requested:
wait_time = 3 wait_time = 3
log.countdown(wait_time, "下一个 Team") log.countdown(wait_time, "下一个 Team")
@@ -645,6 +1298,85 @@ def run_single_team(team_index: int = 0):
return _current_results 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(): def test_email_only():
"""测试模式: 只创建邮箱和邀请,不注册""" """测试模式: 只创建邮箱和邀请,不注册"""
global _tracker global _tracker

View File

@@ -6,12 +6,24 @@
# - 会话标识: S2A 使用 session_id # - 会话标识: S2A 使用 session_id
# - 授权流程: S2A 生成授权 URL -> 用户授权 -> 提交 code 换取 token -> 创建账号 # - 授权流程: S2A 生成授权 URL -> 用户授权 -> 提交 code 换取 token -> 创建账号
# - 账号入库: S2A 可一步完成 (create-from-oauth) 或分步完成 (exchange + add_account) # - 账号入库: S2A 可一步完成 (create-from-oauth) 或分步完成 (exchange + add_account)
#
# 新增: 纯 API 授权模式 (无需浏览器)
# - 使用 curl_cffi 模拟浏览器指纹
# - 支持 Sentinel PoW 验证
# - 直接通过 API 完成 OAuth 流程
import requests import requests
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
from typing import Optional, Tuple, Dict, List, Any 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 ( from config import (
S2A_API_BASE, S2A_API_BASE,
@@ -23,6 +35,8 @@ from config import (
S2A_GROUP_NAMES, S2A_GROUP_NAMES,
REQUEST_TIMEOUT, REQUEST_TIMEOUT,
USER_AGENT, USER_AGENT,
get_next_proxy,
format_proxy_url,
) )
from logger import log from logger import log
@@ -49,6 +63,349 @@ def create_session_with_retry() -> requests.Session:
http_session = create_session_with_retry() 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]: def build_s2a_headers() -> Dict[str, str]:
"""构建 S2A API 请求的 Headers """构建 S2A API 请求的 Headers
@@ -353,7 +710,7 @@ def s2a_create_account_from_oauth(
full_email = name if "@" in name else "" full_email = name if "@" in name else ""
if name: if name:
payload["name"] = name payload["name"] = name if name.startswith("team-") else f"team-{name}"
if proxy_id is not None: if proxy_id is not None:
payload["proxy_id"] = proxy_id payload["proxy_id"] = proxy_id
@@ -433,8 +790,9 @@ def s2a_add_account(
if token_info.get("email"): if token_info.get("email"):
credentials["email"] = token_info.get("email") credentials["email"] = token_info.get("email")
s2a_name = name if name.startswith("team-") else f"team-{name}"
payload = { payload = {
"name": name, "name": s2a_name,
"platform": "openai", "platform": "openai",
"type": "oauth", "type": "oauth",
"credentials": credentials, "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)}") lines.append(f" 费用: {fmt_cost(total_cost)}")
return "\n".join(lines) 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" }, { 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]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.4.4" 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" }, { 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]] [[package]]
name = "datarecorder" name = "datarecorder"
version = "3.6.2" version = "3.6.2"
@@ -337,6 +417,7 @@ name = "oai-team-auto-provisioner"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "curl-cffi" },
{ name = "drissionpage" }, { name = "drissionpage" },
{ name = "python-telegram-bot", extra = ["job-queue"] }, { name = "python-telegram-bot", extra = ["job-queue"] },
{ name = "requests" }, { name = "requests" },
@@ -348,6 +429,7 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "curl-cffi", specifier = ">=0.14.0" },
{ name = "drissionpage", specifier = ">=4.1.1.2" }, { name = "drissionpage", specifier = ">=4.1.1.2" },
{ name = "python-telegram-bot", extras = ["job-queue"], specifier = ">=22.5" }, { name = "python-telegram-bot", extras = ["job-queue"], specifier = ">=22.5" },
{ name = "requests", specifier = ">=2.32.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" }, { 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]] [[package]]
name = "pygments" name = "pygments"
version = "2.19.2" version = "2.19.2"