diff --git a/account_store.py b/account_store.py
new file mode 100644
index 0000000..40091a5
--- /dev/null
+++ b/account_store.py
@@ -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()
diff --git a/bot.py b/bot.py
index 3a175da..df0fdd4 100644
--- a/bot.py
+++ b/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 = (
"🤖 autoClaude Bot\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: {update.effective_user.id}"
)
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 = (
"📖 命令说明\n\n"
- "/register [N]\n"
- " 注册 N 个 Claude 账号(默认 1)。\n"
- " 流程:创建邮箱 → 发送 Magic Link → 等待邮件 → 交换 SessionKey\n"
- " 注册结果自动保存到 accounts.txt\n\n"
- "/check <CARD|MM|YY|CVC>\n"
- " 单张信用卡检查。\n\n"
- "📎 发送 .txt 文件\n"
- " 批量 CC 检查,文件每行一张卡:\n"
- " 卡号|月|年|CVC\n\n"
- "/accounts\n"
- " 列出 accounts.txt 中保存的所有账号。\n\n"
- "/status\n"
- " 查看当前是否有后台任务在运行。\n"
+ "📝 注册与账号\n"
+ " /register [N] — 注册 N 个账号\n"
+ " /accounts — 查看已注册账号\n"
+ " /delete <序号|邮箱> — 删除账号\n"
+ " /verify — 验证 SK 有效性\n\n"
+ "💳 CC 检查\n"
+ " /check — 单张检查\n"
+ " 📎 发送 .txt 文件 — 批量检查\n\n"
+ "🛠 工具与状态\n"
+ " /stop — 中断当前任务\n"
+ " /stats — 统计面板\n"
+ " /status — 任务状态\n\n"
+ "🌐 代理与邮件\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"⏹ 正在停止任务:{_task_name}\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"📬 邮件系统状态(共 {mail_pool.count} 个)\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} {ms.base_url}\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"📋 已注册账号(共 {len(lines)} 个)\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}. {email}\n SK: {sk}\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"🎯 代理测试结果\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} {r['proxy']}\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"🌐 代理池状态(共 {len(items)} 个)\n\n"
+
+ for i, item in enumerate(items, 1):
+ icon = "✅" if item["last_ok"] else "❌"
+ text += (
+ f"{i}. {icon} {item['proxy']}\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"🌐 代理状态: {status}\n"
+ f"📦 代理池: {pp.count} 个(活跃 {pp.active_count} 个)\n\n"
+ f"用法: /proxy on 或 /proxy off",
+ 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"📋 已注册账号(共 {len(accounts)} 个)\n\n"
+ for i, acc in enumerate(accounts, 1):
+ text += f"{i}. {acc['email']}\n SK: {acc['session_key']}\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"
+ " /delete 3 — 按序号删除\n"
+ " /delete user@example.com — 按邮箱删除\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"📧 {removed['email']}\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"🔑 账号验证结果\n\n"
+ f"✅ 有效: {valid} ❌ 无效: {invalid}\n\n"
+ )
+ for i, r in enumerate(results, 1):
+ icon = "✅" if r["ok"] else "❌"
+ text += f"{i}. {icon} {r['email']} — {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 = (
+ "📊 统计面板\n\n"
+ f"📝 注册统计\n"
+ f" 总计: {reg_total} | ✅ {reg_ok} | ❌ {reg_fail}\n"
+ f" 成功率: {reg_rate}\n"
+ )
+
+ # 失败原因分布
+ reasons = stats.get("register_fail_reasons", {})
+ if reasons:
+ text += " 失败原因:\n"
+ for reason, cnt in sorted(reasons.items(), key=lambda x: -x[1]):
+ text += f" • {reason}: {cnt}\n"
+
+ text += (
+ f"\n💳 CC 检查统计\n"
+ f" 总计: {cc_total} | ✅ {cc_ok} | ❌ {cc_fail}\n"
+ f" 通过率: {cc_rate}\n"
+ )
+
+ text += f"\n📦 当前账号数: {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 = "⏹ 注册已中断" if stopped else "🏁 注册完成"
report = (
- f"🏁 注册完成\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失败原因:\n"
+ for reason, cnt in reason_counts.most_common():
+ report += f" • {reason}: {cnt} 次\n"
+
if results["accounts"]:
report += "\n新注册账号:\n"
for acc in results["accounts"]:
- report += f"• {acc.email}\n"
+ report += (
+ f"• {acc.email}\n"
+ f" SK: {acc.session_key}\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 启动中...")
diff --git a/claude_auth.py b/claude_auth.py
index ddbeb1e..04be313 100644
--- a/claude_auth.py
+++ b/claude_auth.py
@@ -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:
diff --git a/config.py b/config.py
index 42fe3db..4c4ea1c 100644
--- a/config.py
+++ b/config.py
@@ -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
diff --git a/config.toml.example b/config.toml.example
index 6471910..19166d6 100644
--- a/config.toml.example
+++ b/config.toml.example
@@ -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(每行一个)
diff --git a/deploy.sh b/deploy.sh
index bb80af6..c9d2c12 100644
--- a/deploy.sh
+++ b/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
-ok "依赖安装完成"
+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
diff --git a/gift_checker.py b/gift_checker.py
index 85ca533..1384a65 100644
--- a/gift_checker.py
+++ b/gift_checker.py
@@ -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:
diff --git a/mail_service.py b/mail_service.py
index 9bef2f9..d8b721a 100644
--- a/mail_service.py
+++ b/mail_service.py
@@ -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)
- if resp.json().get('code') == 200:
- print(f"[+] 邮箱用户创建成功: {full_email}")
- return full_email
- else:
- print(f"[-] 创建邮箱失败: {resp.text}")
+ 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})"
diff --git a/proxy_pool.py b/proxy_pool.py
new file mode 100644
index 0000000..ff3c2fc
--- /dev/null
+++ b/proxy_pool.py
@@ -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
diff --git a/pyproject.toml b/pyproject.toml
index 6375bd2..46caf82 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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",
]
diff --git a/stripe_token.py b/stripe_token.py
index 0efd472..25ba8a0 100644
--- a/stripe_token.py
+++ b/stripe_token.py
@@ -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")
diff --git a/uv.lock b/uv.lock
index 1b01890..00559cd 100644
--- a/uv.lock
+++ b/uv.lock
@@ -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"