feat: Implement thread-safe account and statistics management and integrate proxy support for all external requests.
This commit is contained in:
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 启动中...")
|
||||
|
||||
Reference in New Issue
Block a user