feat: Implement thread-safe account and statistics management and integrate proxy support for all external requests.
This commit is contained in:
180
account_store.py
Normal file
180
account_store.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
线程安全的账号存储模块
|
||||
统一管理 accounts.txt 的读写操作,避免并发冲突。
|
||||
支持删除、统计等功能。
|
||||
"""
|
||||
|
||||
import json
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
_ACCOUNTS_FILE = Path(__file__).parent / "accounts.txt"
|
||||
_STATS_FILE = Path(__file__).parent / "stats.json"
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
# ====== 账号操作 ======
|
||||
|
||||
def append(email: str, session_key: str, org_uuid: str) -> None:
|
||||
"""追加一个账号"""
|
||||
with _lock:
|
||||
with open(_ACCOUNTS_FILE, "a", encoding="utf-8") as f:
|
||||
f.write(f"{email}|{session_key}|{org_uuid}\n")
|
||||
|
||||
|
||||
def read_all() -> list[dict]:
|
||||
"""读取所有账号"""
|
||||
with _lock:
|
||||
try:
|
||||
with open(_ACCOUNTS_FILE, "r", encoding="utf-8") as f:
|
||||
lines = [line.strip() for line in f if line.strip()]
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
|
||||
result = []
|
||||
for line in lines:
|
||||
parts = line.split("|")
|
||||
result.append({
|
||||
"email": parts[0] if len(parts) > 0 else "",
|
||||
"session_key": parts[1] if len(parts) > 1 else "",
|
||||
"org_uuid": parts[2] if len(parts) > 2 else "",
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def read_lines() -> list[str]:
|
||||
"""读取所有原始行"""
|
||||
with _lock:
|
||||
try:
|
||||
with open(_ACCOUNTS_FILE, "r", encoding="utf-8") as f:
|
||||
return [line.strip() for line in f if line.strip()]
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
|
||||
|
||||
def get_last() -> dict | None:
|
||||
"""获取最后一个账号"""
|
||||
accounts = read_all()
|
||||
return accounts[-1] if accounts else None
|
||||
|
||||
|
||||
def get_last_line() -> str | None:
|
||||
"""获取最后一行原始数据"""
|
||||
lines = read_lines()
|
||||
return lines[-1] if lines else None
|
||||
|
||||
|
||||
def count() -> int:
|
||||
"""账号总数"""
|
||||
return len(read_all())
|
||||
|
||||
|
||||
def delete_by_index(index: int) -> dict | None:
|
||||
"""删除指定序号的账号(1-based),返回被删除的账号信息"""
|
||||
with _lock:
|
||||
try:
|
||||
with open(_ACCOUNTS_FILE, "r", encoding="utf-8") as f:
|
||||
lines = [line.strip() for line in f if line.strip()]
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
if index < 1 or index > len(lines):
|
||||
return None
|
||||
|
||||
removed_line = lines.pop(index - 1)
|
||||
with open(_ACCOUNTS_FILE, "w", encoding="utf-8") as f:
|
||||
for line in lines:
|
||||
f.write(line + "\n")
|
||||
|
||||
parts = removed_line.split("|")
|
||||
return {
|
||||
"email": parts[0] if len(parts) > 0 else "",
|
||||
"session_key": parts[1] if len(parts) > 1 else "",
|
||||
"org_uuid": parts[2] if len(parts) > 2 else "",
|
||||
}
|
||||
|
||||
|
||||
def delete_by_email(email: str) -> dict | None:
|
||||
"""按邮箱删除账号"""
|
||||
with _lock:
|
||||
try:
|
||||
with open(_ACCOUNTS_FILE, "r", encoding="utf-8") as f:
|
||||
lines = [line.strip() for line in f if line.strip()]
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
removed = None
|
||||
remaining = []
|
||||
for line in lines:
|
||||
parts = line.split("|")
|
||||
if parts[0] == email and removed is None:
|
||||
removed = {
|
||||
"email": parts[0] if len(parts) > 0 else "",
|
||||
"session_key": parts[1] if len(parts) > 1 else "",
|
||||
"org_uuid": parts[2] if len(parts) > 2 else "",
|
||||
}
|
||||
else:
|
||||
remaining.append(line)
|
||||
|
||||
if removed:
|
||||
with open(_ACCOUNTS_FILE, "w", encoding="utf-8") as f:
|
||||
for line in remaining:
|
||||
f.write(line + "\n")
|
||||
|
||||
return removed
|
||||
|
||||
|
||||
# ====== 统计数据 ======
|
||||
|
||||
def _load_stats() -> dict:
|
||||
try:
|
||||
with open(_STATS_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return {
|
||||
"register_total": 0,
|
||||
"register_success": 0,
|
||||
"register_fail": 0,
|
||||
"register_fail_reasons": {},
|
||||
"cc_total": 0,
|
||||
"cc_pass": 0,
|
||||
"cc_fail": 0,
|
||||
}
|
||||
|
||||
|
||||
def _save_stats(stats: dict):
|
||||
with open(_STATS_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(stats, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def record_register(success: bool, fail_reason: str = ""):
|
||||
"""记录一次注册结果"""
|
||||
with _lock:
|
||||
stats = _load_stats()
|
||||
stats["register_total"] += 1
|
||||
if success:
|
||||
stats["register_success"] += 1
|
||||
else:
|
||||
stats["register_fail"] += 1
|
||||
if fail_reason:
|
||||
reasons = stats.setdefault("register_fail_reasons", {})
|
||||
reasons[fail_reason] = reasons.get(fail_reason, 0) + 1
|
||||
_save_stats(stats)
|
||||
|
||||
|
||||
def record_cc(passed: bool):
|
||||
"""记录一次 CC 检查结果"""
|
||||
with _lock:
|
||||
stats = _load_stats()
|
||||
stats["cc_total"] += 1
|
||||
if passed:
|
||||
stats["cc_pass"] += 1
|
||||
else:
|
||||
stats["cc_fail"] += 1
|
||||
_save_stats(stats)
|
||||
|
||||
|
||||
def get_stats() -> dict:
|
||||
"""获取统计数据"""
|
||||
with _lock:
|
||||
return _load_stats()
|
||||
537
bot.py
537
bot.py
@@ -5,16 +5,19 @@ autoClaude Telegram Bot
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
import threading
|
||||
from functools import wraps
|
||||
|
||||
from telegram import Update, BotCommand
|
||||
from telegram import Update, BotCommand, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.ext import (
|
||||
Application,
|
||||
CallbackQueryHandler,
|
||||
CommandHandler,
|
||||
MessageHandler,
|
||||
ContextTypes,
|
||||
@@ -30,6 +33,8 @@ from mail_service import MailPool, extract_magic_link
|
||||
from stripe_token import StripeTokenizer
|
||||
from gift_checker import GiftChecker
|
||||
from claude_auth import attack_claude, finalize_login
|
||||
import account_store
|
||||
import proxy_pool
|
||||
|
||||
# --- 日志配置 ---
|
||||
logging.basicConfig(
|
||||
@@ -37,11 +42,14 @@ logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
# 降低 httpx 轮询日志级别,减少刷屏
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
|
||||
# --- 全局状态 ---
|
||||
_task_lock = threading.Lock()
|
||||
_task_running = False
|
||||
_task_name = ""
|
||||
_stop_event = threading.Event()
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -68,6 +76,7 @@ def _set_task(name: str) -> bool:
|
||||
return False
|
||||
_task_running = True
|
||||
_task_name = name
|
||||
_stop_event.clear()
|
||||
return True
|
||||
|
||||
|
||||
@@ -77,6 +86,12 @@ def _clear_task():
|
||||
with _task_lock:
|
||||
_task_running = False
|
||||
_task_name = ""
|
||||
_stop_event.clear()
|
||||
|
||||
|
||||
def _is_stopped() -> bool:
|
||||
"""检查是否收到停止信号"""
|
||||
return _stop_event.is_set()
|
||||
|
||||
|
||||
async def _edit_or_send(msg, text: str):
|
||||
@@ -87,6 +102,16 @@ async def _edit_or_send(msg, text: str):
|
||||
pass
|
||||
|
||||
|
||||
def _progress_bar(current: int, total: int, width: int = 12) -> str:
|
||||
"""生成可视化进度条 ▓▓▓▓░░░░ 3/10 (30%)"""
|
||||
if total <= 0:
|
||||
return ""
|
||||
pct = current / total
|
||||
filled = round(width * pct)
|
||||
bar = "▓" * filled + "░" * (width - filled)
|
||||
return f"{bar} {current}/{total} ({round(pct * 100)}%)"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 命令处理
|
||||
# ============================================================
|
||||
@@ -97,12 +122,17 @@ async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
welcome = (
|
||||
"🤖 <b>autoClaude Bot</b>\n\n"
|
||||
"可用命令:\n"
|
||||
" /register [N] — 注册 Claude 账号(默认 1 个)\n"
|
||||
" /check <卡号|月|年|CVC> — 单张 CC 检查\n"
|
||||
" /register [N] — 注册 Claude 账号\n"
|
||||
" /check <卡号|月|年|CVC> — CC 检查\n"
|
||||
" 📎 发送 .txt 文件 — 批量 CC 检查\n"
|
||||
" /accounts — 查看已注册账号\n"
|
||||
" /status — 当前任务状态\n"
|
||||
" /help — 帮助\n\n"
|
||||
" /accounts — 查看账号 | /delete — 删除账号\n"
|
||||
" /verify — 验证 SK 有效性\n"
|
||||
" /stats — 统计面板\n"
|
||||
" /stop — 中断当前任务\n"
|
||||
" /proxy on|off — 代理开关\n"
|
||||
" /proxytest — 测试代理 | /proxystatus — 代理状态\n"
|
||||
" /mailstatus — 邮件系统状态\n"
|
||||
" /status — 任务状态 | /help — 帮助\n\n"
|
||||
f"👤 你的用户 ID: <code>{update.effective_user.id}</code>"
|
||||
)
|
||||
await update.message.reply_text(welcome, parse_mode="HTML")
|
||||
@@ -113,19 +143,23 @@ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/help — 命令列表"""
|
||||
text = (
|
||||
"📖 <b>命令说明</b>\n\n"
|
||||
"<b>/register [N]</b>\n"
|
||||
" 注册 N 个 Claude 账号(默认 1)。\n"
|
||||
" 流程:创建邮箱 → 发送 Magic Link → 等待邮件 → 交换 SessionKey\n"
|
||||
" 注册结果自动保存到 accounts.txt\n\n"
|
||||
"<b>/check <CARD|MM|YY|CVC></b>\n"
|
||||
" 单张信用卡检查。\n\n"
|
||||
"<b>📎 发送 .txt 文件</b>\n"
|
||||
" 批量 CC 检查,文件每行一张卡:\n"
|
||||
" <code>卡号|月|年|CVC</code>\n\n"
|
||||
"<b>/accounts</b>\n"
|
||||
" 列出 accounts.txt 中保存的所有账号。\n\n"
|
||||
"<b>/status</b>\n"
|
||||
" 查看当前是否有后台任务在运行。\n"
|
||||
"<b>📝 注册与账号</b>\n"
|
||||
" /register [N] — 注册 N 个账号\n"
|
||||
" /accounts — 查看已注册账号\n"
|
||||
" /delete <序号|邮箱> — 删除账号\n"
|
||||
" /verify — 验证 SK 有效性\n\n"
|
||||
"<b>💳 CC 检查</b>\n"
|
||||
" /check <CARD|MM|YY|CVC> — 单张检查\n"
|
||||
" 📎 发送 .txt 文件 — 批量检查\n\n"
|
||||
"<b>🛠 工具与状态</b>\n"
|
||||
" /stop — 中断当前任务\n"
|
||||
" /stats — 统计面板\n"
|
||||
" /status — 任务状态\n\n"
|
||||
"<b>🌐 代理与邮件</b>\n"
|
||||
" /proxy on|off — 开关代理\n"
|
||||
" /proxytest — 测试代理\n"
|
||||
" /proxystatus — 代理池状态\n"
|
||||
" /mailstatus — 邮件系统状态\n"
|
||||
)
|
||||
await update.message.reply_text(text, parse_mode="HTML")
|
||||
|
||||
@@ -144,30 +178,301 @@ async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
|
||||
|
||||
@restricted
|
||||
async def cmd_accounts(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/accounts — 列出已注册账号"""
|
||||
try:
|
||||
with open("accounts.txt", "r") as f:
|
||||
lines = [l.strip() for l in f if l.strip()]
|
||||
except FileNotFoundError:
|
||||
await update.message.reply_text("📭 尚无已注册账号(accounts.txt 不存在)。")
|
||||
async def cmd_stop(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/stop — 中断当前运行的任务"""
|
||||
with _task_lock:
|
||||
if not _task_running:
|
||||
await update.message.reply_text("✅ 当前没有运行中的任务。")
|
||||
return
|
||||
_stop_event.set()
|
||||
await update.message.reply_text(
|
||||
f"⏹ 正在停止任务:<b>{_task_name}</b>\n"
|
||||
"任务将在当前步骤完成后安全退出...",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@restricted
|
||||
async def cmd_mailstatus(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/mailstatus — 检查邮箱系统连通性"""
|
||||
status_msg = await update.message.reply_text("🔍 正在检测邮件系统...")
|
||||
|
||||
mail_pool = MailPool(MAIL_SYSTEMS)
|
||||
if mail_pool.count == 0:
|
||||
await _edit_or_send(status_msg, "❌ 没有可用的邮箱系统!请检查 config.toml 配置。")
|
||||
return
|
||||
|
||||
if not lines:
|
||||
await update.message.reply_text("📭 accounts.txt 为空。")
|
||||
text = f"📬 <b>邮件系统状态(共 {mail_pool.count} 个)</b>\n\n"
|
||||
for i, ms in enumerate(mail_pool.systems, 1):
|
||||
health = ms.check_health()
|
||||
icon = "✅" if health["ok"] else "❌"
|
||||
domains = ", ".join(ms.domains)
|
||||
text += f"{i}. {icon} <code>{ms.base_url}</code>\n"
|
||||
text += f" 域名: {domains}\n"
|
||||
text += f" 状态: {health['message']}\n"
|
||||
|
||||
await _edit_or_send(status_msg, text)
|
||||
|
||||
|
||||
@restricted
|
||||
async def cmd_proxytest(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/proxytest — 测试所有代理的连通性"""
|
||||
pp = proxy_pool.pool
|
||||
if pp.count == 0:
|
||||
await update.message.reply_text("❌ 代理池为空(proxy.txt 不存在或无有效代理)")
|
||||
return
|
||||
|
||||
text = f"📋 <b>已注册账号(共 {len(lines)} 个)</b>\n\n"
|
||||
for i, line in enumerate(lines, 1):
|
||||
parts = line.split("|")
|
||||
email = parts[0] if len(parts) > 0 else "?"
|
||||
sk = parts[1][:12] + "..." if len(parts) > 1 and len(parts[1]) > 12 else (parts[1] if len(parts) > 1 else "?")
|
||||
text += f"{i}. <code>{email}</code>\n SK: <code>{sk}</code>\n"
|
||||
status_msg = await update.message.reply_text(
|
||||
f"🔍 正在测试 {pp.count} 个代理...(可能需要一些时间)"
|
||||
)
|
||||
|
||||
# 在线程中执行测试避免阻塞事件循环
|
||||
loop = asyncio.get_event_loop()
|
||||
results = await loop.run_in_executor(None, pp.test_all)
|
||||
|
||||
# 构建结果报告
|
||||
ok_count = sum(1 for r in results if r["ok"])
|
||||
fail_count = len(results) - ok_count
|
||||
|
||||
text = (
|
||||
f"🎯 <b>代理测试结果</b>\n\n"
|
||||
f"✅ 通过: {ok_count} ❌ 失败: {fail_count} "
|
||||
f"🚧 剩余可用: {pp.active_count}\n\n"
|
||||
)
|
||||
|
||||
for r in results:
|
||||
icon = "✅" if r["ok"] else "❌"
|
||||
latency = f"{r['latency_ms']}ms" if r['latency_ms'] > 0 else "-"
|
||||
prio = r.get('priority', '-')
|
||||
text += f"{icon} <code>{r['proxy']}</code>\n"
|
||||
text += f" 延迟: {latency} | 优先级: {prio}\n"
|
||||
if not r["ok"]:
|
||||
text += f" 错误: {r.get('error', '?')}\n"
|
||||
|
||||
# Telegram 消息限制 4096 字符
|
||||
if len(text) > 4000:
|
||||
text = text[:4000] + "\n...(已截断)"
|
||||
|
||||
await _edit_or_send(status_msg, text)
|
||||
|
||||
|
||||
@restricted
|
||||
async def cmd_proxystatus(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/proxystatus — 查看代理池状态"""
|
||||
pp = proxy_pool.pool
|
||||
if pp.count == 0:
|
||||
await update.message.reply_text("❌ 代理池为空(proxy.txt 不存在或无有效代理)")
|
||||
return
|
||||
|
||||
items = pp.status_list()
|
||||
text = f"🌐 <b>代理池状态(共 {len(items)} 个)</b>\n\n"
|
||||
|
||||
for i, item in enumerate(items, 1):
|
||||
icon = "✅" if item["last_ok"] else "❌"
|
||||
text += (
|
||||
f"{i}. {icon} <code>{item['proxy']}</code>\n"
|
||||
f" 优先级: {item['priority']} | "
|
||||
f"延迟: {item['latency_ms']}ms | "
|
||||
f"✅{item['success']} ❌{item['fail']}\n"
|
||||
)
|
||||
|
||||
if len(text) > 4000:
|
||||
text = text[:4000] + "\n...(已截断)"
|
||||
|
||||
await update.message.reply_text(text, parse_mode="HTML")
|
||||
|
||||
|
||||
@restricted
|
||||
async def cmd_proxy(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/proxy on|off — 开关代理"""
|
||||
pp = proxy_pool.pool
|
||||
|
||||
if not context.args:
|
||||
# 无参数:显示当前状态
|
||||
status = "✅ 已开启" if pp.enabled else "❌ 已关闭"
|
||||
await update.message.reply_text(
|
||||
f"🌐 <b>代理状态</b>: {status}\n"
|
||||
f"📦 代理池: {pp.count} 个(活跃 {pp.active_count} 个)\n\n"
|
||||
f"用法: <code>/proxy on</code> 或 <code>/proxy off</code>",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
return
|
||||
|
||||
arg = context.args[0].lower()
|
||||
if arg == "on":
|
||||
pp.enabled = True
|
||||
await update.message.reply_text(
|
||||
f"✅ 代理已开启(池中 {pp.active_count} 个可用)"
|
||||
)
|
||||
elif arg == "off":
|
||||
pp.enabled = False
|
||||
await update.message.reply_text("❌ 代理已关闭,所有请求将直连")
|
||||
else:
|
||||
await update.message.reply_text("❌ 用法: /proxy on 或 /proxy off")
|
||||
|
||||
|
||||
@restricted
|
||||
async def cmd_accounts(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/accounts — 列出已注册账号"""
|
||||
accounts = account_store.read_all()
|
||||
|
||||
if not accounts:
|
||||
await update.message.reply_text("📭 尚无已注册账号。")
|
||||
return
|
||||
|
||||
text = f"📋 <b>已注册账号(共 {len(accounts)} 个)</b>\n\n"
|
||||
for i, acc in enumerate(accounts, 1):
|
||||
text += f"{i}. <code>{acc['email']}</code>\n SK: <code>{acc['session_key']}</code>\n"
|
||||
|
||||
if len(text) > 4000:
|
||||
text = text[:4000] + "\n...(已截断,请点击下方按钮导出完整数据)"
|
||||
|
||||
keyboard = [[InlineKeyboardButton("📥 导出 JSON 文件", callback_data="export_accounts_json")]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
await update.message.reply_text(text, parse_mode="HTML", reply_markup=reply_markup)
|
||||
|
||||
|
||||
@restricted
|
||||
async def cmd_delete(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/delete <序号|邮箱> — 删除指定账号"""
|
||||
user_id = update.effective_user.id
|
||||
|
||||
if not context.args:
|
||||
await update.message.reply_text(
|
||||
"❌ 用法:\n"
|
||||
" <code>/delete 3</code> — 按序号删除\n"
|
||||
" <code>/delete user@example.com</code> — 按邮箱删除\n\n"
|
||||
"💡 使用 /accounts 查看序号",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
return
|
||||
|
||||
arg = context.args[0]
|
||||
removed = None
|
||||
|
||||
# 尝试按序号删除
|
||||
try:
|
||||
index = int(arg)
|
||||
removed = account_store.delete_by_index(index)
|
||||
if not removed:
|
||||
total = account_store.count()
|
||||
await update.message.reply_text(f"❌ 无效序号。当前共 {total} 个账号。")
|
||||
return
|
||||
except ValueError:
|
||||
# 按邮箱删除
|
||||
removed = account_store.delete_by_email(arg)
|
||||
if not removed:
|
||||
await update.message.reply_text(f"❌ 未找到邮箱:{arg}")
|
||||
return
|
||||
|
||||
await update.message.reply_text(
|
||||
f"🗑 已删除账号:\n"
|
||||
f"📧 <code>{removed['email']}</code>\n"
|
||||
f"剩余 {account_store.count()} 个账号",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@restricted
|
||||
async def cmd_verify(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/verify — 检测已保存的 Session Key 是否仍然有效"""
|
||||
accounts = account_store.read_all()
|
||||
|
||||
if not accounts:
|
||||
await update.message.reply_text("📭 尚无已注册账号。")
|
||||
return
|
||||
|
||||
status_msg = await update.message.reply_text(
|
||||
f"🔍 正在验证 {len(accounts)} 个账号的 Session Key..."
|
||||
)
|
||||
|
||||
from curl_cffi import requests as cffi_requests
|
||||
from config import get_proxy
|
||||
|
||||
results = []
|
||||
for i, acc in enumerate(accounts, 1):
|
||||
sk = acc.get("session_key", "")
|
||||
email = acc.get("email", "?")
|
||||
if not sk:
|
||||
results.append({"email": email, "ok": False, "reason": "SK 为空"})
|
||||
continue
|
||||
|
||||
try:
|
||||
resp = cffi_requests.get(
|
||||
"https://claude.ai/api/organizations",
|
||||
headers={
|
||||
"Cookie": f"sessionKey={sk}",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
},
|
||||
impersonate="chrome124",
|
||||
proxies=get_proxy(),
|
||||
timeout=15,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
results.append({"email": email, "ok": True, "reason": "有效"})
|
||||
elif resp.status_code == 401:
|
||||
results.append({"email": email, "ok": False, "reason": "已过期"})
|
||||
elif resp.status_code == 403:
|
||||
results.append({"email": email, "ok": False, "reason": "被封禁"})
|
||||
else:
|
||||
results.append({"email": email, "ok": False, "reason": f"HTTP {resp.status_code}"})
|
||||
except Exception as e:
|
||||
results.append({"email": email, "ok": False, "reason": str(e)[:50]})
|
||||
|
||||
valid = sum(1 for r in results if r["ok"])
|
||||
invalid = len(results) - valid
|
||||
|
||||
text = (
|
||||
f"🔑 <b>账号验证结果</b>\n\n"
|
||||
f"✅ 有效: {valid} ❌ 无效: {invalid}\n\n"
|
||||
)
|
||||
for i, r in enumerate(results, 1):
|
||||
icon = "✅" if r["ok"] else "❌"
|
||||
text += f"{i}. {icon} <code>{r['email']}</code> — {r['reason']}\n"
|
||||
|
||||
if len(text) > 4000:
|
||||
text = text[:4000] + "\n...(已截断)"
|
||||
|
||||
await _edit_or_send(status_msg, text)
|
||||
|
||||
|
||||
@restricted
|
||||
async def cmd_stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/stats — 展示历史统计数据"""
|
||||
stats = account_store.get_stats()
|
||||
|
||||
reg_total = stats.get("register_total", 0)
|
||||
reg_ok = stats.get("register_success", 0)
|
||||
reg_fail = stats.get("register_fail", 0)
|
||||
reg_rate = f"{round(reg_ok / reg_total * 100)}%" if reg_total > 0 else "-"
|
||||
|
||||
cc_total = stats.get("cc_total", 0)
|
||||
cc_ok = stats.get("cc_pass", 0)
|
||||
cc_fail = stats.get("cc_fail", 0)
|
||||
cc_rate = f"{round(cc_ok / cc_total * 100)}%" if cc_total > 0 else "-"
|
||||
|
||||
text = (
|
||||
"📊 <b>统计面板</b>\n\n"
|
||||
f"<b>📝 注册统计</b>\n"
|
||||
f" 总计: {reg_total} | ✅ {reg_ok} | ❌ {reg_fail}\n"
|
||||
f" 成功率: {reg_rate}\n"
|
||||
)
|
||||
|
||||
# 失败原因分布
|
||||
reasons = stats.get("register_fail_reasons", {})
|
||||
if reasons:
|
||||
text += " <b>失败原因:</b>\n"
|
||||
for reason, cnt in sorted(reasons.items(), key=lambda x: -x[1]):
|
||||
text += f" • {reason}: {cnt}\n"
|
||||
|
||||
text += (
|
||||
f"\n<b>💳 CC 检查统计</b>\n"
|
||||
f" 总计: {cc_total} | ✅ {cc_ok} | ❌ {cc_fail}\n"
|
||||
f" 通过率: {cc_rate}\n"
|
||||
)
|
||||
|
||||
text += f"\n<b>📦 当前账号数</b>: {account_store.count()}"
|
||||
|
||||
await update.message.reply_text(text, parse_mode="HTML")
|
||||
|
||||
|
||||
@@ -210,7 +515,7 @@ async def cmd_register(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
|
||||
def _register_worker(loop: asyncio.AbstractEventLoop, status_msg, count: int):
|
||||
"""注册工作线程"""
|
||||
results = {"success": 0, "fail": 0, "accounts": []}
|
||||
results = {"success": 0, "fail": 0, "accounts": [], "fail_reasons": []}
|
||||
|
||||
try:
|
||||
# 初始化邮箱系统池
|
||||
@@ -233,99 +538,159 @@ def _register_worker(loop: asyncio.AbstractEventLoop, status_msg, count: int):
|
||||
).result(timeout=10)
|
||||
|
||||
for i in range(count):
|
||||
progress = f"[{i + 1}/{count}]"
|
||||
bar = _progress_bar(i, count)
|
||||
step_header = f"📊 {bar}\n"
|
||||
|
||||
# Step 1: 创建邮箱(轮询选系统)
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
_edit_or_send(
|
||||
status_msg,
|
||||
f"⏳ {progress} 创建临时邮箱...\n\n"
|
||||
f"{step_header}⏳ 创建临时邮箱...\n\n"
|
||||
f"✅ 成功: {results['success']} ❌ 失败: {results['fail']}",
|
||||
),
|
||||
loop,
|
||||
).result(timeout=10)
|
||||
|
||||
# 检查停止信号
|
||||
if _is_stopped():
|
||||
break
|
||||
|
||||
random_prefix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10))
|
||||
target_email, mail_sys = mail_pool.create_user(random_prefix)
|
||||
|
||||
if not target_email or not mail_sys:
|
||||
results["fail"] += 1
|
||||
reason = "邮箱创建失败"
|
||||
results["fail_reasons"].append(reason)
|
||||
account_store.record_register(False, reason)
|
||||
continue
|
||||
|
||||
# 检查停止信号
|
||||
if _is_stopped():
|
||||
break
|
||||
|
||||
# Step 2: 发送 Magic Link
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
_edit_or_send(
|
||||
status_msg,
|
||||
f"⏳ {progress} 发送 Magic Link...\n"
|
||||
f"{step_header}⏳ 发送 Magic Link...\n"
|
||||
f"📧 {target_email}\n\n"
|
||||
f"✅ 成功: {results['success']} ❌ 失败: {results['fail']}",
|
||||
),
|
||||
loop,
|
||||
).result(timeout=10)
|
||||
|
||||
if _is_stopped():
|
||||
break
|
||||
|
||||
if not attack_claude(target_email):
|
||||
results["fail"] += 1
|
||||
reason = "Magic Link 发送失败"
|
||||
results["fail_reasons"].append(reason)
|
||||
account_store.record_register(False, reason)
|
||||
continue
|
||||
|
||||
# Step 3: 等待邮件(使用创建时对应的系统)
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
_edit_or_send(
|
||||
status_msg,
|
||||
f"⏳ {progress} 等待 Claude 邮件...\n"
|
||||
f"{step_header}⏳ 等待 Claude 邮件...\n"
|
||||
f"📧 {target_email}\n\n"
|
||||
f"✅ 成功: {results['success']} ❌ 失败: {results['fail']}",
|
||||
),
|
||||
loop,
|
||||
).result(timeout=10)
|
||||
|
||||
email_content = mail_sys.wait_for_email(target_email)
|
||||
email_content = mail_sys.wait_for_email(target_email, stop_check=_is_stopped)
|
||||
if _is_stopped():
|
||||
break
|
||||
if not email_content:
|
||||
results["fail"] += 1
|
||||
reason = "邮件接收超时"
|
||||
results["fail_reasons"].append(reason)
|
||||
account_store.record_register(False, reason)
|
||||
continue
|
||||
|
||||
magic_link = extract_magic_link(email_content)
|
||||
if not magic_link:
|
||||
results["fail"] += 1
|
||||
reason = "Magic Link 解析失败"
|
||||
results["fail_reasons"].append(reason)
|
||||
account_store.record_register(False, reason)
|
||||
continue
|
||||
|
||||
# Step 4: 交换 SessionKey
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
_edit_or_send(
|
||||
status_msg,
|
||||
f"⏳ {progress} 交换 SessionKey...\n"
|
||||
f"{step_header}⏳ 交换 SessionKey...\n"
|
||||
f"📧 {target_email}\n\n"
|
||||
f"✅ 成功: {results['success']} ❌ 失败: {results['fail']}",
|
||||
),
|
||||
loop,
|
||||
).result(timeout=10)
|
||||
|
||||
if _is_stopped():
|
||||
break
|
||||
|
||||
account = finalize_login(magic_link)
|
||||
if account:
|
||||
results["success"] += 1
|
||||
results["accounts"].append(account)
|
||||
# 保存到文件
|
||||
with open("accounts.txt", "a") as f:
|
||||
f.write(f"{account.email}|{account.session_key}|{account.org_uuid}\n")
|
||||
account_store.append(account.email, account.session_key, account.org_uuid)
|
||||
account_store.record_register(True)
|
||||
else:
|
||||
results["fail"] += 1
|
||||
reason = "SessionKey 交换失败"
|
||||
results["fail_reasons"].append(reason)
|
||||
account_store.record_register(False, reason)
|
||||
|
||||
# 间隔防止限流
|
||||
if i < count - 1:
|
||||
time.sleep(2)
|
||||
|
||||
# 最终汇报
|
||||
stopped = _is_stopped()
|
||||
done_bar = _progress_bar(results["success"] + results["fail"], count)
|
||||
title = "⏹ <b>注册已中断</b>" if stopped else "🏁 <b>注册完成</b>"
|
||||
report = (
|
||||
f"🏁 <b>注册完成</b>\n\n"
|
||||
f"{title}\n"
|
||||
f"📊 {done_bar}\n\n"
|
||||
f"✅ 成功: {results['success']}\n"
|
||||
f"❌ 失败: {results['fail']}\n"
|
||||
)
|
||||
|
||||
# 失败原因汇总
|
||||
if results["fail_reasons"]:
|
||||
from collections import Counter
|
||||
reason_counts = Counter(results["fail_reasons"])
|
||||
report += "\n<b>失败原因:</b>\n"
|
||||
for reason, cnt in reason_counts.most_common():
|
||||
report += f" • {reason}: {cnt} 次\n"
|
||||
|
||||
if results["accounts"]:
|
||||
report += "\n<b>新注册账号:</b>\n"
|
||||
for acc in results["accounts"]:
|
||||
report += f"• <code>{acc.email}</code>\n"
|
||||
report += (
|
||||
f"• <code>{acc.email}</code>\n"
|
||||
f" SK: <code>{acc.session_key}</code>\n"
|
||||
)
|
||||
|
||||
# Telegram 消息限制 4096 字符
|
||||
if len(report) > 4000:
|
||||
report = report[:4000] + "\n...(已截断,使用 /accounts 查看完整列表)"
|
||||
|
||||
keyboard = [[InlineKeyboardButton("📥 导出 JSON 文件", callback_data="export_accounts_json")]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
async def _send_final():
|
||||
try:
|
||||
await status_msg.edit_text(report, parse_mode="HTML", reply_markup=reply_markup)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
_edit_or_send(status_msg, report),
|
||||
_send_final(),
|
||||
loop,
|
||||
).result(timeout=10)
|
||||
|
||||
@@ -361,13 +726,9 @@ async def cmd_check(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
return
|
||||
|
||||
# 读取可用账号
|
||||
try:
|
||||
with open("accounts.txt", "r") as f:
|
||||
lines = [l.strip() for l in f if l.strip()]
|
||||
except FileNotFoundError:
|
||||
lines = []
|
||||
acc_lines = account_store.read_lines()
|
||||
|
||||
if not lines:
|
||||
if not acc_lines:
|
||||
await update.message.reply_text("❌ 没有可用账号,请先 /register 注册一个。")
|
||||
return
|
||||
|
||||
@@ -383,7 +744,7 @@ async def cmd_check(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
loop = asyncio.get_event_loop()
|
||||
threading.Thread(
|
||||
target=_check_worker,
|
||||
args=(loop, status_msg, card_line, lines[-1]),
|
||||
args=(loop, status_msg, card_line, acc_lines[-1]),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
@@ -477,11 +838,7 @@ async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
return
|
||||
|
||||
# 读取可用账号
|
||||
try:
|
||||
with open("accounts.txt", "r") as f:
|
||||
acc_lines = [l.strip() for l in f if l.strip()]
|
||||
except FileNotFoundError:
|
||||
acc_lines = []
|
||||
acc_lines = account_store.read_lines()
|
||||
|
||||
if not acc_lines:
|
||||
await update.message.reply_text("❌ 没有可用账号,请先 /register 注册一个。")
|
||||
@@ -548,6 +905,9 @@ def _batch_check_worker(loop: asyncio.AbstractEventLoop, status_msg, cards: list
|
||||
checker = GiftChecker(account)
|
||||
|
||||
for i, card_line in enumerate(cards):
|
||||
if _is_stopped():
|
||||
break
|
||||
|
||||
cc, mm, yy, cvc = card_line.split("|")
|
||||
masked = f"{cc[:4]}****{cc[-4:]}"
|
||||
|
||||
@@ -615,6 +975,40 @@ def _batch_check_worker(loop: asyncio.AbstractEventLoop, status_msg, cards: list
|
||||
_clear_task()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 回调处理 — 导出 JSON
|
||||
# ============================================================
|
||||
|
||||
async def callback_export_json(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""处理导出 JSON 文件的回调"""
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
accounts = account_store.read_all()
|
||||
|
||||
if not accounts:
|
||||
await query.message.reply_text("📭 没有账号数据。")
|
||||
return
|
||||
|
||||
json_data = json.dumps(accounts, indent=2, ensure_ascii=False)
|
||||
json_path = "accounts_export.json"
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
f.write(json_data)
|
||||
|
||||
with open(json_path, "rb") as f:
|
||||
await query.message.reply_document(
|
||||
document=f,
|
||||
filename="accounts.json",
|
||||
caption=f"📋 共 {len(accounts)} 个账号",
|
||||
)
|
||||
|
||||
# 清理临时文件
|
||||
try:
|
||||
os.remove(json_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 启动 Bot
|
||||
# ============================================================
|
||||
@@ -624,9 +1018,17 @@ async def post_init(application: Application):
|
||||
commands = [
|
||||
BotCommand("start", "欢迎信息"),
|
||||
BotCommand("register", "注册 Claude 账号 [数量]"),
|
||||
BotCommand("check", "CC 检查 <卡号|月|年|CVC>"),
|
||||
BotCommand("accounts", "查看已注册账号"),
|
||||
BotCommand("status", "当前任务状态"),
|
||||
BotCommand("stop", "中断当前任务"),
|
||||
BotCommand("check", "CC 检查"),
|
||||
BotCommand("accounts", "查看账号"),
|
||||
BotCommand("delete", "删除账号"),
|
||||
BotCommand("verify", "验证 SK 有效性"),
|
||||
BotCommand("stats", "统计面板"),
|
||||
BotCommand("mailstatus", "邮件系统状态"),
|
||||
BotCommand("proxy", "代理开关 on/off"),
|
||||
BotCommand("proxytest", "测试代理"),
|
||||
BotCommand("proxystatus", "代理池状态"),
|
||||
BotCommand("status", "任务状态"),
|
||||
BotCommand("help", "命令帮助"),
|
||||
]
|
||||
await application.bot.set_my_commands(commands)
|
||||
@@ -645,9 +1047,18 @@ def main():
|
||||
app.add_handler(CommandHandler("start", cmd_start))
|
||||
app.add_handler(CommandHandler("help", cmd_help))
|
||||
app.add_handler(CommandHandler("register", cmd_register))
|
||||
app.add_handler(CommandHandler("stop", cmd_stop))
|
||||
app.add_handler(CommandHandler("check", cmd_check))
|
||||
app.add_handler(CommandHandler("accounts", cmd_accounts))
|
||||
app.add_handler(CommandHandler("delete", cmd_delete))
|
||||
app.add_handler(CommandHandler("verify", cmd_verify))
|
||||
app.add_handler(CommandHandler("stats", cmd_stats))
|
||||
app.add_handler(CommandHandler("mailstatus", cmd_mailstatus))
|
||||
app.add_handler(CommandHandler("proxytest", cmd_proxytest))
|
||||
app.add_handler(CommandHandler("proxystatus", cmd_proxystatus))
|
||||
app.add_handler(CommandHandler("proxy", cmd_proxy))
|
||||
app.add_handler(CommandHandler("status", cmd_status))
|
||||
app.add_handler(CallbackQueryHandler(callback_export_json, pattern="^export_accounts_json$"))
|
||||
app.add_handler(MessageHandler(filters.Document.ALL, handle_document))
|
||||
|
||||
logger.info("🤖 Bot 启动中...")
|
||||
|
||||
@@ -2,7 +2,7 @@ import uuid
|
||||
import base64
|
||||
from curl_cffi import requests # 用于模拟指纹
|
||||
|
||||
from config import CLAUDE_URL
|
||||
from config import CLAUDE_URL, get_proxy
|
||||
from models import ClaudeAccount
|
||||
from identity import random_ua
|
||||
|
||||
@@ -48,7 +48,8 @@ def attack_claude(target_email):
|
||||
CLAUDE_URL,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
impersonate="chrome124"
|
||||
impersonate="chrome124",
|
||||
proxies=get_proxy(),
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
@@ -132,7 +133,8 @@ def finalize_login(magic_link_fragment):
|
||||
verify_url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
impersonate="chrome124"
|
||||
impersonate="chrome124",
|
||||
proxies=get_proxy(),
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
|
||||
@@ -31,3 +31,7 @@ TG_ALLOWED_USERS: list[int] = _cfg["telegram"].get("allowed_users", [])
|
||||
|
||||
# --- 邮箱系统 ---
|
||||
MAIL_SYSTEMS: list[dict] = _cfg.get("mail", [])
|
||||
|
||||
# --- 代理池 ---
|
||||
# 代理逻辑统一由 proxy_pool.py 管理,这里只做 re-export 保持兼容
|
||||
from proxy_pool import get_proxy, get_proxy_count # noqa: E402, F401
|
||||
|
||||
@@ -30,3 +30,6 @@ domains = ["example.com"]
|
||||
# base_url = "https://mail2.example.com/"
|
||||
# api_token = "your_api_token_here"
|
||||
# domains = ["domain2.com", "domain3.com"]
|
||||
|
||||
# --- 代理配置 ---
|
||||
# 代理从 proxy.txt 文件加载,格式: host:port:user:pass(每行一个)
|
||||
|
||||
26
deploy.sh
26
deploy.sh
@@ -44,11 +44,13 @@ echo ""
|
||||
# ============================================================
|
||||
info "检查系统依赖..."
|
||||
|
||||
# 确保 uv 路径在 PATH 中
|
||||
export PATH="$HOME/.local/bin:/root/.local/bin:$PATH"
|
||||
|
||||
# 安装 uv(如果不存在)
|
||||
if ! command -v uv &> /dev/null; then
|
||||
info "安装 uv..."
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
ok "uv 已安装"
|
||||
else
|
||||
ok "uv 已存在 ($(uv --version))"
|
||||
@@ -59,8 +61,16 @@ fi
|
||||
# ============================================================
|
||||
info "安装 Python 依赖..."
|
||||
cd "$APP_DIR"
|
||||
sudo -u "$RUN_USER" uv sync 2>/dev/null || sudo -u "$RUN_USER" uv pip install -r pyproject.toml 2>/dev/null || true
|
||||
if uv sync; then
|
||||
ok "依赖安装完成"
|
||||
else
|
||||
warn "uv sync 失败,尝试 uv pip install..."
|
||||
if uv pip install -r pyproject.toml; then
|
||||
ok "依赖安装完成 (pip fallback)"
|
||||
else
|
||||
err "依赖安装失败,请手动检查"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 3. 检查配置文件
|
||||
@@ -83,7 +93,16 @@ fi
|
||||
# ============================================================
|
||||
# 4. 获取 uv 和 python 路径
|
||||
# ============================================================
|
||||
UV_PATH="$(sudo -u "$RUN_USER" bash -c 'which uv')"
|
||||
UV_PATH="$(which uv 2>/dev/null || echo '')"
|
||||
if [ -z "$UV_PATH" ]; then
|
||||
# 尝试常见路径
|
||||
for p in "$HOME/.local/bin/uv" "/root/.local/bin/uv" "/usr/local/bin/uv"; do
|
||||
if [ -x "$p" ]; then UV_PATH="$p"; break; fi
|
||||
done
|
||||
fi
|
||||
if [ -z "$UV_PATH" ]; then
|
||||
err "找不到 uv,请检查安装是否成功"
|
||||
fi
|
||||
info "uv 路径: ${UV_PATH}"
|
||||
|
||||
# ============================================================
|
||||
@@ -119,7 +138,6 @@ SyslogIdentifier=${APP_NAME}
|
||||
# 安全加固
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
ReadWritePaths=${APP_DIR}
|
||||
PrivateTmp=true
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from curl_cffi import requests # 用于模拟指纹
|
||||
|
||||
from config import PRODUCT_ID
|
||||
from config import PRODUCT_ID, get_proxy
|
||||
from models import ClaudeAccount
|
||||
from identity import random_address
|
||||
|
||||
@@ -42,7 +42,7 @@ class GiftChecker:
|
||||
|
||||
try:
|
||||
print(f"[*] 正在尝试扣款 (Gift Purchase)...")
|
||||
resp = requests.post(url, json=payload, headers=headers, impersonate="chrome124")
|
||||
resp = requests.post(url, json=payload, headers=headers, impersonate="chrome124", proxies=get_proxy())
|
||||
|
||||
resp_json = {}
|
||||
try:
|
||||
|
||||
@@ -4,6 +4,8 @@ import random
|
||||
import threading
|
||||
import requests as standard_requests # 用于普通API交互
|
||||
|
||||
from config import get_proxy
|
||||
|
||||
|
||||
class MailSystem:
|
||||
"""单个邮箱系统实例,支持多域名"""
|
||||
@@ -32,16 +34,23 @@ class MailSystem:
|
||||
}
|
||||
]
|
||||
}
|
||||
resp = standard_requests.post(url, json=payload, headers=self.headers)
|
||||
try:
|
||||
resp = standard_requests.post(url, json=payload, headers=self.headers, proxies=get_proxy(), timeout=15)
|
||||
if resp.json().get('code') == 200:
|
||||
print(f"[+] 邮箱用户创建成功: {full_email}")
|
||||
return full_email
|
||||
elif resp.status_code in (401, 403):
|
||||
print(f"[-] 邮箱 API Token 无效或已过期! HTTP {resp.status_code}")
|
||||
return None
|
||||
else:
|
||||
print(f"[-] 创建邮箱失败: {resp.text}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[-] 创建邮箱请求异常: {e}")
|
||||
return None
|
||||
|
||||
def wait_for_email(self, to_email, retry_count=20, sleep_time=3):
|
||||
"""像猎人一样耐心等待猎物出现"""
|
||||
def wait_for_email(self, to_email, retry_count=20, sleep_time=3, stop_check=None):
|
||||
"""像猎人一样耐心等待猎物出现,支持外部中断"""
|
||||
url = f"{self.base_url}/api/public/emailList"
|
||||
payload = {
|
||||
"toEmail": to_email,
|
||||
@@ -54,10 +63,19 @@ class MailSystem:
|
||||
print(f"[*] 开始轮询邮件,目标: {to_email}...")
|
||||
|
||||
for i in range(retry_count):
|
||||
# 检查外部中断信号
|
||||
if stop_check and stop_check():
|
||||
print("[!] 收到停止信号,中断邮件轮询")
|
||||
return None
|
||||
|
||||
try:
|
||||
resp = standard_requests.post(url, json=payload, headers=self.headers)
|
||||
resp = standard_requests.post(url, json=payload, headers=self.headers, proxies=get_proxy(), timeout=15)
|
||||
data = resp.json()
|
||||
|
||||
if resp.status_code in (401, 403):
|
||||
print(f"[-] 邮箱 API Token 无效或已过期! HTTP {resp.status_code}")
|
||||
return None
|
||||
|
||||
if data.get('code') == 200 and data.get('data'):
|
||||
emails = data['data']
|
||||
for email in emails:
|
||||
@@ -73,6 +91,27 @@ class MailSystem:
|
||||
print("[-] 等待超时,未收到邮件。")
|
||||
return None
|
||||
|
||||
def check_health(self) -> dict:
|
||||
"""检查该邮箱系统的连通性和 Token 有效性"""
|
||||
if not self.token:
|
||||
return {"ok": False, "message": "Token 未配置"}
|
||||
try:
|
||||
url = f"{self.base_url}/api/public/emailList"
|
||||
payload = {"toEmail": "health@check.test", "sendName": "", "num": 1, "size": 1}
|
||||
resp = standard_requests.post(url, json=payload, headers=self.headers, proxies=get_proxy(), timeout=10)
|
||||
if resp.status_code == 200:
|
||||
return {"ok": True, "message": "连接正常"}
|
||||
elif resp.status_code in (401, 403):
|
||||
return {"ok": False, "message": f"Token 无效 (HTTP {resp.status_code})"}
|
||||
else:
|
||||
return {"ok": False, "message": f"异常响应 (HTTP {resp.status_code})"}
|
||||
except standard_requests.exceptions.ConnectTimeout:
|
||||
return {"ok": False, "message": "连接超时"}
|
||||
except standard_requests.exceptions.ConnectionError:
|
||||
return {"ok": False, "message": "无法连接"}
|
||||
except Exception as e:
|
||||
return {"ok": False, "message": f"异常: {e}"}
|
||||
|
||||
def __repr__(self):
|
||||
return f"MailSystem({self.base_url}, domains={self.domains})"
|
||||
|
||||
|
||||
276
proxy_pool.py
Normal file
276
proxy_pool.py
Normal file
@@ -0,0 +1,276 @@
|
||||
"""
|
||||
代理池管理模块
|
||||
功能:
|
||||
- 从 proxy.txt 加载代理(支持 host:port:user:pass 格式)
|
||||
- 基于优先级的智能选取(优先使用表现好的代理)
|
||||
- 自动测试连通性和延迟
|
||||
- 测试失败降低优先级,过低则淘汰
|
||||
- 线程安全
|
||||
"""
|
||||
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
import requests as std_requests
|
||||
|
||||
|
||||
# --- 配置常量 ---
|
||||
_PROXY_FILE = Path(__file__).parent / "proxy.txt"
|
||||
_TEST_URL = "https://claude.ai" # 测试目标
|
||||
_TEST_TIMEOUT = 10 # 测试超时秒数
|
||||
_INITIAL_PRIORITY = 100 # 初始优先级
|
||||
_FAIL_PENALTY = 30 # 每次失败扣分
|
||||
_SUCCESS_BONUS = 10 # 每次成功加分
|
||||
_MAX_PRIORITY = 100 # 最高优先级
|
||||
_REMOVE_THRESHOLD = 0 # 优先级低于此值则淘汰
|
||||
|
||||
|
||||
@dataclass
|
||||
class Proxy:
|
||||
"""代理实例"""
|
||||
raw: str # 原始行
|
||||
url: str # 解析后的 URL (http://user:pass@host:port)
|
||||
host: str
|
||||
port: str
|
||||
priority: int = _INITIAL_PRIORITY
|
||||
latency: float = 0.0 # 最近一次测试延迟 (ms)
|
||||
fail_count: int = 0
|
||||
success_count: int = 0
|
||||
last_test_time: float = 0.0
|
||||
last_test_ok: bool = True
|
||||
|
||||
@property
|
||||
def masked_url(self) -> str:
|
||||
"""脱敏显示"""
|
||||
if "@" in self.url:
|
||||
prefix = self.url.split("@")[0]
|
||||
suffix = self.url.split("@")[1]
|
||||
# 隐藏密码
|
||||
if ":" in prefix.replace("http://", "").replace("https://", ""):
|
||||
user_part = prefix.split(":")[-2].split("/")[-1]
|
||||
return f"{self.host}:{self.port} ({user_part[:8]}...)"
|
||||
return f"{self.host}:{self.port}"
|
||||
|
||||
|
||||
def _parse_line(line: str) -> Proxy | None:
|
||||
"""解析一行代理配置"""
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
return None
|
||||
|
||||
parts = line.split(":")
|
||||
if len(parts) == 4:
|
||||
host, port, user, passwd = parts
|
||||
url = f"http://{user}:{passwd}@{host}:{port}"
|
||||
return Proxy(raw=line, url=url, host=host, port=port)
|
||||
elif len(parts) == 2:
|
||||
host, port = parts
|
||||
url = f"http://{host}:{port}"
|
||||
return Proxy(raw=line, url=url, host=host, port=port)
|
||||
elif line.startswith(("http://", "https://", "socks5://")):
|
||||
# 从完整 URL 提取 host:port
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(line)
|
||||
return Proxy(raw=line, url=line, host=parsed.hostname or "?", port=str(parsed.port or "?"))
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
class ProxyPool:
|
||||
"""线程安全的代理池"""
|
||||
|
||||
def __init__(self):
|
||||
self._proxies: list[Proxy] = []
|
||||
self._lock = threading.Lock()
|
||||
self.enabled = True # 代理开关
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
"""从 proxy.txt 加载代理"""
|
||||
if not _PROXY_FILE.exists():
|
||||
print("[*] 未找到 proxy.txt,不使用代理")
|
||||
return
|
||||
|
||||
with open(_PROXY_FILE, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
proxy = _parse_line(line)
|
||||
if proxy:
|
||||
self._proxies.append(proxy)
|
||||
|
||||
if self._proxies:
|
||||
print(f"[+] 代理池: 已加载 {len(self._proxies)} 个代理")
|
||||
else:
|
||||
print("[!] proxy.txt 存在但没有有效代理")
|
||||
|
||||
def reload(self):
|
||||
"""重新加载 proxy.txt"""
|
||||
with self._lock:
|
||||
self._proxies.clear()
|
||||
self._load()
|
||||
|
||||
@property
|
||||
def count(self) -> int:
|
||||
return len(self._proxies)
|
||||
|
||||
@property
|
||||
def active_count(self) -> int:
|
||||
"""有效代理数量"""
|
||||
return sum(1 for p in self._proxies if p.priority > _REMOVE_THRESHOLD)
|
||||
|
||||
def get(self) -> dict:
|
||||
"""
|
||||
基于优先级加权随机选取一个代理,返回 requests 格式的 proxies dict。
|
||||
代理关闭或无可用代理时返回空 dict(直连)。
|
||||
"""
|
||||
if not self.enabled:
|
||||
return {}
|
||||
with self._lock:
|
||||
alive = [p for p in self._proxies if p.priority > _REMOVE_THRESHOLD]
|
||||
if not alive:
|
||||
return {}
|
||||
|
||||
# 加权随机:priority 越高越容易选中
|
||||
weights = [p.priority for p in alive]
|
||||
chosen = random.choices(alive, weights=weights, k=1)[0]
|
||||
return {"http": chosen.url, "https": chosen.url}
|
||||
|
||||
def report_success(self, proxies: dict):
|
||||
"""调用方报告该代理请求成功"""
|
||||
if not proxies:
|
||||
return
|
||||
url = proxies.get("https", "")
|
||||
with self._lock:
|
||||
for p in self._proxies:
|
||||
if p.url == url:
|
||||
p.success_count += 1
|
||||
p.priority = min(p.priority + _SUCCESS_BONUS, _MAX_PRIORITY)
|
||||
break
|
||||
|
||||
def report_failure(self, proxies: dict):
|
||||
"""调用方报告该代理请求失败,降低优先级"""
|
||||
if not proxies:
|
||||
return
|
||||
url = proxies.get("https", "")
|
||||
with self._lock:
|
||||
for p in self._proxies:
|
||||
if p.url == url:
|
||||
p.fail_count += 1
|
||||
p.priority -= _FAIL_PENALTY
|
||||
if p.priority <= _REMOVE_THRESHOLD:
|
||||
print(f"[!] 代理已淘汰 (优先级归零): {p.masked_url}")
|
||||
break
|
||||
|
||||
def _cleanup(self):
|
||||
"""移除优先级过低的代理"""
|
||||
before = len(self._proxies)
|
||||
self._proxies = [p for p in self._proxies if p.priority > _REMOVE_THRESHOLD]
|
||||
removed = before - len(self._proxies)
|
||||
if removed:
|
||||
print(f"[!] 清理了 {removed} 个失效代理,剩余 {len(self._proxies)} 个")
|
||||
self._save()
|
||||
|
||||
def _save(self):
|
||||
"""将当前有效代理写回 proxy.txt"""
|
||||
with open(_PROXY_FILE, "w", encoding="utf-8") as f:
|
||||
for p in self._proxies:
|
||||
f.write(p.raw + "\n")
|
||||
|
||||
def test_one(self, proxy: Proxy) -> dict:
|
||||
"""测试单个代理,返回结果 dict"""
|
||||
proxies = {"http": proxy.url, "https": proxy.url}
|
||||
try:
|
||||
start = time.time()
|
||||
resp = std_requests.get(
|
||||
_TEST_URL,
|
||||
proxies=proxies,
|
||||
timeout=_TEST_TIMEOUT,
|
||||
allow_redirects=True,
|
||||
)
|
||||
latency = (time.time() - start) * 1000 # ms
|
||||
|
||||
proxy.latency = latency
|
||||
proxy.last_test_time = time.time()
|
||||
|
||||
if resp.status_code < 500:
|
||||
proxy.last_test_ok = True
|
||||
proxy.success_count += 1
|
||||
proxy.priority = min(proxy.priority + _SUCCESS_BONUS, _MAX_PRIORITY)
|
||||
return {"ok": True, "latency_ms": round(latency), "status": resp.status_code}
|
||||
else:
|
||||
proxy.last_test_ok = False
|
||||
proxy.fail_count += 1
|
||||
proxy.priority -= _FAIL_PENALTY
|
||||
return {"ok": False, "latency_ms": round(latency), "error": f"HTTP {resp.status_code}"}
|
||||
|
||||
except std_requests.exceptions.ConnectTimeout:
|
||||
proxy.last_test_ok = False
|
||||
proxy.fail_count += 1
|
||||
proxy.priority -= _FAIL_PENALTY
|
||||
proxy.last_test_time = time.time()
|
||||
return {"ok": False, "latency_ms": -1, "error": "连接超时"}
|
||||
except std_requests.exceptions.ProxyError as e:
|
||||
proxy.last_test_ok = False
|
||||
proxy.fail_count += 1
|
||||
proxy.priority -= _FAIL_PENALTY
|
||||
proxy.last_test_time = time.time()
|
||||
return {"ok": False, "latency_ms": -1, "error": f"代理错误: {e}"}
|
||||
except Exception as e:
|
||||
proxy.last_test_ok = False
|
||||
proxy.fail_count += 1
|
||||
proxy.priority -= _FAIL_PENALTY
|
||||
proxy.last_test_time = time.time()
|
||||
return {"ok": False, "latency_ms": -1, "error": str(e)}
|
||||
|
||||
def test_all(self) -> list[dict]:
|
||||
"""
|
||||
测试所有代理,返回结果列表。
|
||||
测试后自动清理优先级过低的代理。
|
||||
"""
|
||||
results = []
|
||||
with self._lock:
|
||||
proxies_snapshot = list(self._proxies)
|
||||
|
||||
for proxy in proxies_snapshot:
|
||||
result = self.test_one(proxy)
|
||||
result["proxy"] = proxy.masked_url
|
||||
result["priority"] = proxy.priority
|
||||
results.append(result)
|
||||
|
||||
with self._lock:
|
||||
self._cleanup()
|
||||
|
||||
return results
|
||||
|
||||
def status_list(self) -> list[dict]:
|
||||
"""返回所有代理的状态信息"""
|
||||
with self._lock:
|
||||
return [
|
||||
{
|
||||
"proxy": p.masked_url,
|
||||
"priority": p.priority,
|
||||
"latency_ms": round(p.latency) if p.latency else "-",
|
||||
"success": p.success_count,
|
||||
"fail": p.fail_count,
|
||||
"last_ok": p.last_test_ok,
|
||||
}
|
||||
for p in self._proxies
|
||||
]
|
||||
|
||||
|
||||
# --- 全局单例 ---
|
||||
pool = ProxyPool()
|
||||
|
||||
|
||||
def get_proxy() -> dict:
|
||||
"""供外部模块调用:随机获取一个代理"""
|
||||
return pool.get()
|
||||
|
||||
|
||||
def get_proxy_count() -> int:
|
||||
"""代理池大小"""
|
||||
return pool.count
|
||||
@@ -6,6 +6,7 @@ readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"curl-cffi>=0.14.0",
|
||||
"faker>=36.0.0",
|
||||
"python-telegram-bot>=21.0",
|
||||
"requests>=2.32.5",
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@ import uuid
|
||||
import random
|
||||
from curl_cffi import requests # 用于模拟指纹
|
||||
|
||||
from config import STRIPE_PK
|
||||
from config import STRIPE_PK, get_proxy
|
||||
from identity import random_address, random_name
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class StripeTokenizer:
|
||||
|
||||
try:
|
||||
print(f"[*] 正在向 Stripe 请求 Token: {cc_num[:4]}******{cc_num[-4:]}")
|
||||
resp = requests.post(url, data=data, headers=headers, impersonate="chrome124")
|
||||
resp = requests.post(url, data=data, headers=headers, impersonate="chrome124", proxies=get_proxy())
|
||||
|
||||
if resp.status_code == 200:
|
||||
pm_id = resp.json().get("id")
|
||||
|
||||
23
uv.lock
generated
23
uv.lock
generated
@@ -25,6 +25,7 @@ version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "curl-cffi" },
|
||||
{ name = "faker" },
|
||||
{ name = "python-telegram-bot" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
@@ -32,6 +33,7 @@ dependencies = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "curl-cffi", specifier = ">=0.14.0" },
|
||||
{ name = "faker", specifier = ">=36.0.0" },
|
||||
{ name = "python-telegram-bot", specifier = ">=21.0" },
|
||||
{ name = "requests", specifier = ">=2.32.5" },
|
||||
]
|
||||
@@ -182,6 +184,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/7c/d2ba86b0b3e1e2830bd94163d047de122c69a8df03c5c7c36326c456ad82/curl_cffi-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:2eed50a969201605c863c4c31269dfc3e0da52916086ac54553cfa353022425c", size = 1425067, upload-time = "2025-12-16T03:25:06.454Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "40.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/7e/dccb7013c9f3d66f2e379383600629fec75e4da2698548bdbf2041ea4b51/faker-40.4.0.tar.gz", hash = "sha256:76f8e74a3df28c3e2ec2caafa956e19e37a132fdc7ea067bc41783affcfee364", size = 1952221, upload-time = "2026-02-06T23:30:15.515Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/63/58efa67c10fb27810d34351b7a10f85f109a7f7e2a07dc3773952459c47b/faker-40.4.0-py3-none-any.whl", hash = "sha256:486d43c67ebbb136bc932406418744f9a0bdf2c07f77703ea78b58b77e9aa443", size = 1987060, upload-time = "2026-02-06T23:30:13.44Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
@@ -274,6 +288,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
|
||||
Reference in New Issue
Block a user