backup current state

This commit is contained in:
skernelx committed 2026-03-16 01:23:43 +08:00
commit e98e237d47
19 files changed
+3593

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/
# 账号信息
accounts.txt
accounts.csv
# IDE
.vscode/
.idea/
*.swp
*.swo
# 系统
.DS_Store
+170
View File
@@ -0,0 +1,170 @@
# Tavily 全自动注册工具
自动注册 Tavily 账号并获取 API Key,支持邮件验证的完整自动化工具。
## ✨ 核心特性
- 🚀 **零配置启动** - 自动创建虚拟环境并安装依赖
- 🔐 **本地 Turnstile Solver** - 免费稳定的验证码解决方案
- 📧 **邮件自动验证** - 使用 Cloudflare Email Workers 自动接收验证邮件
- 💾 **自动保存账号** - 格式兼容代理服务器直接导入
- 🎨 **交互式界面** - 简洁友好的命令行交互
- 🌐 **跨平台支持** - Windows / macOS / Linux
## 🚀 快速开始
### 一键运行
```bash
python3 run.py
```
就这么简单!脚本会自动:
1. 创建虚拟环境
2. 安装所有依赖
3. 下载浏览器
4. 启动 Turnstile Solver
5. 进入交互界面
### 使用流程
1. 运行 `python3 run.py`
2. 选择 `1` 开始注册
3. 输入注册数量(默认 5)
4. 输入间隔秒数(默认 10)
5. 选择是否上传到代理服务器
6. 等待注册完成
生成的账号保存在 `accounts.txt`,格式:
```
email,password,api_key
email,password,api_key
...
```
## 📁 项目结构
```
tavily-key-generator/
├── run.py # 主程序(一键启动)
├── tavily_core.py # 注册核心逻辑
├── api_solver.py # Turnstile Solver
├── config.py # 配置文件
├── requirements.txt # Python 依赖
├── browser_configs.py # Solver 浏览器配置
├── db_results.py # Solver 数据库
├── proxy/ # 代理服务器(可选部署)
│ ├── server.py
│ ├── database.py
│ ├── key_pool.py
│ └── templates/
└── README.md
```
## ⚙️ 配置说明
编辑 `config.py` 自定义配置:
```python
# 邮箱配置(Cloudflare Email Workers)
EMAIL_API_URL = "https://mail.nashome.me"
EMAIL_API_TOKEN = "your-token"
EMAIL_DOMAIN = "nashome.me"
# 代理服务器配置(上传目标)
SERVER_URL = "https://tavily.hunters.works"
SERVER_ADMIN_PASSWORD = "your-password"
# 默认参数
DEFAULT_COUNT = 5 # 默认注册数量
DEFAULT_DELAY = 10 # 每次注册间隔(秒)
```
## 🔧 技术架构
- **HTTP 客户端**: curl_cffi(模拟真实浏览器 TLS 指纹)
- **验证码破解**: 本地 Turnstile Solver(Camoufox 浏览器自动化)
- **邮件接收**: Cloudflare Email Workers API
- **认证流程**: Auth0 Universal Login
## 📦 依赖说明
所有依赖会自动安装,无需手动操作:
- `curl_cffi` - HTTP 客户端(绕过 Cloudflare)
- `requests` - 标准 HTTP 库
- `quart` - 异步 Web 框架(Solver)
- `camoufox` - 反检测浏览器(Solver)
- `patchright` - Playwright 分支(Solver)
- `rich` - 终端美化(Solver)
## 🎯 代理服务器
项目内置代理服务器(`proxy/` 目录),支持:
- API Key 池管理
- 自动轮询使用
- 配额限制
- 使用统计
- Web 控制台
### 部署代理服务器
```bash
cd proxy/
docker compose up -d
```
访问 `http://localhost:9874` 进入管理控制台。
### 批量导入 API Key
在控制台中选择"批量导入",粘贴 `accounts.txt` 内容即可。
## ⚠️ 注意事项
- 首次运行会下载 Camoufox 浏览器(约 200MB)
- Solver 启动需要 10-15 秒初始化时间
- 每个 API Key 有 1000 次免费调用额度(需要邮件验证激活)
- 建议批量注册时设置 10-15 秒延迟
- 邮件验证最多等待 120 秒
## 🐛 故障排查
**Solver 无法启动**:
```bash
# 检查端口占用
lsof -i :5072
# 手动清理进程
kill -9 $(lsof -ti :5072)
```
**注册失败**:
- 确认 Solver 正在运行(会显示"✅ Solver 已启动")
- 检查网络连接
- 确认邮箱 API Token 有效
**邮件验证超时**:
- 检查 `config.py` 中的 `EMAIL_API_TOKEN` 是否正确
- 访问 https://mail.nashome.me 测试邮箱服务是否正常
- 增加等待时间(修改 `tavily_core.py` 中的 `timeout` 参数)
**虚拟环境问题**:
```bash
# 删除虚拟环境重新创建
rm -rf venv
python3 run.py
```
## 📄 输出文件
- `accounts.txt` - 账号信息(email,password,api_key)
- `venv/` - 虚拟环境(自动创建,已加入 .gitignore)
## 🤝 贡献
欢迎提交 Issue 和 Pull Request!
## 📜 许可证
MIT License
+1123
View File
File diff suppressed because it is too large. Load diff
+17
View File
@@ -0,0 +1,17 @@
import random
class browser_config:
@staticmethod
def get_random_browser_config(browser_type):
# 返回: 浏览器名, 版本, User-Agent, Sec-CH-UA
versions = ["120.0.0.0", "121.0.0.0", "122.0.0.0", "124.0.0.0"]
ver = random.choice(versions)
ua = f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{ver} Safari/537.36"
sec_ch_ua = f'"Not(A:Brand";v="99", "Google Chrome";v="{ver.split(".")[0]}", "Chromium";v="{ver.split(".")[0]}"'
return "chrome", ver, ua, sec_ch_ua
@staticmethod
def get_browser_config(name, version):
ua = f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{version} Safari/537.36"
sec_ch_ua = f'"Google Chrome";v="{version}", "Chromium";v="{version}"'
return ua, sec_ch_ua
+23
View File
@@ -0,0 +1,23 @@
"""
Tavily 注册工具 - 配置文件
修改此文件以匹配你的环境
"""
# ──���────────── 邮箱配置 ─────────────
# Cloudflare Email Workers
EMAIL_API_URL = "https://mail.nashome.me"
EMAIL_API_TOKEN = "0e5ad283dc25405dfdeb47fbb876648ce95d9daadda92d4175a17e3e426c0d4b"
EMAIL_DOMAIN = "nashome.me"
# DuckMail
DUCKMAIL_API_URL = "https://api.duckmail.sbs"
DUCKMAIL_API_KEY = "dk_05d110d3bb873e617406f0946fb9e55f02f6ab926aeac06ac7419e9ac62d5d08"
# ───────────── 代理服务器配置(上传目标)─────────────
SERVER_URL = "https://tavily.hunters.works"
SERVER_ADMIN_PASSWORD = "Jelly120425"
# ───────────── 注册默认参数 ─────────────
DEFAULT_COUNT = 5 # 默认注册数量
DEFAULT_DELAY = 10 # 每次注册间隔(秒)
FIXED_PASSWORD = "Tavily2024!@" # 固定密码(方便登录)
+27
View File
@@ -0,0 +1,27 @@
import time
import asyncio
# 内存数据库,用于临时存储验证码结果
results_db = {}
async def init_db():
print("[系统] 结果数据库初始化成功 (内存模式)")
async def save_result(task_id, task_type, data):
# 存储结果,如果 data 是字典则存入,否则构造字典
results_db[task_id] = data
print(f"[系统] 任务 {task_id} 状态更新: {data.get('value', '正在处理')}")
async def load_result(task_id):
return results_db.get(task_id)
async def cleanup_old_results(days_old=7):
# 简单的清理逻辑
now = time.time()
to_delete = []
for tid, res in results_db.items():
if isinstance(res, dict) and now - res.get('createTime', now) > days_old * 86400:
to_delete.append(tid)
for tid in to_delete:
del results_db[tid]
return len(to_delete)
+1
View File
@@ -0,0 +1 @@
ADMIN_PASSWORD=change-me
+7
View File
@@ -0,0 +1,7 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 9874
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "9874"]
+106
View File
@@ -0,0 +1,106 @@
# Tavily API Proxy
将多个 Tavily API Key 池化,对外暴露统一端点和 Token,附带 Web 管理控制台。
## 功能
- **Key 池化轮询**:Round-robin 分配请求到多个 API Key,连续失败 3 次自动禁用
- **Token 管理**:创建多个访问 Token,每个 Token 独立配额(小时/日/月)
- **用量统计**:实时查看成功/失败次数、延迟、配额使用情况
- **Web 控制台**:可视化管理 Key、Token 和用量
- **批量导入**:支持从 `api_keys.md` 格式文本批量导入 Key
- **兼容 Tavily 官方 API**:客户端只需改 base URL 即可
## 快速开始
### Docker 部署(推荐)
```bash
cd proxy/
# 修改管理密码
cp .env.example .env
# 编辑 .env 中的 ADMIN_PASSWORD
# 启动
docker compose up -d
```
服务运行在 `http://localhost:9874`。
### 本地运行
```bash
cd proxy/
pip install -r requirements.txt
ADMIN_PASSWORD=your-password uvicorn server:app --host 0.0.0.0 --port 9874
```
## 使用流程
1. 访问 `http://localhost:9874/`,输入管理密码登录
2. 在 Key 管理中导入 Tavily API Key(支持单个添加或批量导入)
3. 创建 Token,复制 Token ID
4. 在应用中使用代理端点:
```bash
curl -X POST http://localhost:9874/api/search \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "hello world"}'
```
也可以把 Token 放在 body 的 `api_key` 字段中:
```bash
curl -X POST http://localhost:9874/api/search \
-H "Content-Type: application/json" \
-d '{"query": "hello world", "api_key": "YOUR_TOKEN"}'
```
## API 端点
### 代理端点
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/search` | 代理 Tavily Search API |
| POST | `/api/extract` | 代理 Tavily Extract API |
认证方式:`Authorization: Bearer {token}` 或 body 中 `api_key` 字段。
### 管理端点
所有管理端点需要 `X-Admin-Password` 请求头或 `Authorization: Bearer {ADMIN_PASSWORD}`。
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/` | Web 管理控制台 |
| GET | `/api/stats` | 用量统计概览 |
| GET | `/api/keys` | 列出所有 Key(脱敏显示) |
| POST | `/api/keys` | 添加 Key:`{"key":"tvly-xxx"}` 或批量 `{"file":"文本内容"}` |
| DELETE | `/api/keys/{id}` | 删除 Key |
| PUT | `/api/keys/{id}/toggle` | 启用/禁用 Key:`{"active": 1}` |
| GET | `/api/tokens` | 列出所有 Token |
| POST | `/api/tokens` | 创建 Token:`{"name":"备注"}` |
| DELETE | `/api/tokens/{id}` | 删除 Token |
## 配置
| 环境变量 | 默认值 | 说明 |
|----------|--------|------|
| `ADMIN_PASSWORD` | `admin` | 管理控制台和管理 API 的密码 |
## Token 配额
每个 Token 默认配额:
- 小时限制:100 次
- 日限制:500 次
- 月限制:5000 次
超出配额返回 `429 Too Many Requests`。
## 数据持久化
SQLite 数据库存储在 `data/proxy.db`。Docker 部署时通过 volume 挂载 `./data:/app/data` 持久化。
+272
View File
@@ -0,0 +1,272 @@
"""
SQLite 数据库管理
"""
import sqlite3
import os
from datetime import datetime, timezone
DB_PATH = os.path.join(os.path.dirname(__file__), "data", "proxy.db")
def get_conn():
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
return conn
def init_db():
conn = get_conn()
conn.executescript("""
CREATE TABLE IF NOT EXISTS api_keys (
id INTEGER PRIMARY KEY,
key TEXT UNIQUE NOT NULL,
email TEXT,
active INTEGER DEFAULT 1,
total_used INTEGER DEFAULT 0,
total_failed INTEGER DEFAULT 0,
consecutive_fails INTEGER DEFAULT 0,
last_used_at TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS tokens (
id INTEGER PRIMARY KEY,
token TEXT UNIQUE NOT NULL,
name TEXT DEFAULT '',
hourly_limit INTEGER DEFAULT 0,
daily_limit INTEGER DEFAULT 0,
monthly_limit INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS usage_logs (
id INTEGER PRIMARY KEY,
token_id INTEGER,
api_key_id INTEGER,
endpoint TEXT,
success INTEGER,
latency_ms INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_usage_created ON usage_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_usage_token ON usage_logs(token_id);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
""")
conn.commit()
conn.close()
# ═══ Settings ═══
def get_setting(key, default=None):
conn = get_conn()
try:
row = conn.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
return row["value"] if row else default
finally:
conn.close()
def set_setting(key, value):
conn = get_conn()
try:
conn.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", (key, value))
conn.commit()
finally:
conn.close()
# ═══ API Keys ═══
def add_key(key, email=""):
conn = get_conn()
try:
conn.execute("INSERT OR IGNORE INTO api_keys (key, email) VALUES (?, ?)", (key, email))
conn.commit()
return conn.execute("SELECT * FROM api_keys WHERE key = ?", (key,)).fetchone()
finally:
conn.close()
def get_all_keys():
conn = get_conn()
try:
return conn.execute("SELECT * FROM api_keys ORDER BY id").fetchall()
finally:
conn.close()
def get_active_keys():
conn = get_conn()
try:
return conn.execute("SELECT * FROM api_keys WHERE active = 1 ORDER BY id").fetchall()
finally:
conn.close()
def update_key_usage(key_id, success):
conn = get_conn()
try:
now = datetime.now(timezone.utc).isoformat()
if success:
conn.execute(
"UPDATE api_keys SET total_used = total_used + 1, consecutive_fails = 0, last_used_at = ? WHERE id = ?",
(now, key_id),
)
else:
conn.execute(
"UPDATE api_keys SET total_failed = total_failed + 1, consecutive_fails = consecutive_fails + 1, last_used_at = ? WHERE id = ?",
(now, key_id),
)
# 连续失败 3 次自动禁用
row = conn.execute("SELECT consecutive_fails FROM api_keys WHERE id = ?", (key_id,)).fetchone()
if row and row["consecutive_fails"] >= 3:
conn.execute("UPDATE api_keys SET active = 0 WHERE id = ?", (key_id,))
conn.commit()
finally:
conn.close()
def toggle_key(key_id, active):
conn = get_conn()
try:
conn.execute("UPDATE api_keys SET active = ?, consecutive_fails = 0 WHERE id = ?", (active, key_id))
conn.commit()
finally:
conn.close()
def delete_key(key_id):
conn = get_conn()
try:
conn.execute("DELETE FROM api_keys WHERE id = ?", (key_id,))
conn.commit()
finally:
conn.close()
def import_keys_from_text(text):
"""从 api_keys.md 格式文本批量导入 key"""
import re
count = 0
for line in text.strip().split("\n"):
line = line.strip()
if not line:
continue
# 格式: email,password,tvly-xxx,timestamp;
match = re.search(r"(tvly-[A-Za-z0-9\-_]{20,})", line)
if match:
key = match.group(1)
parts = line.split(",")
email = parts[0] if len(parts) >= 3 else ""
add_key(key, email)
count += 1
return count
# ═══ Tokens ═══
def create_token(name=""):
import random
import string
# 生成类似真实 Tavily key 的长 token: tvly-xxxxxxxx...
token = "tvly-" + "".join(random.choices(string.ascii_letters + string.digits, k=32))
conn = get_conn()
try:
conn.execute("INSERT INTO tokens (token, name) VALUES (?, ?)", (token, name))
conn.commit()
return conn.execute("SELECT * FROM tokens WHERE token = ?", (token,)).fetchone()
finally:
conn.close()
def get_all_tokens():
conn = get_conn()
try:
return conn.execute("SELECT * FROM tokens ORDER BY id").fetchall()
finally:
conn.close()
def get_token_by_value(token_value):
conn = get_conn()
try:
return conn.execute("SELECT * FROM tokens WHERE token = ?", (token_value,)).fetchone()
finally:
conn.close()
def delete_token(token_id):
conn = get_conn()
try:
conn.execute("DELETE FROM tokens WHERE id = ?", (token_id,))
conn.commit()
finally:
conn.close()
# ═══ Usage Logs ═══
def log_usage(token_id, api_key_id, endpoint, success, latency_ms):
conn = get_conn()
try:
conn.execute(
"INSERT INTO usage_logs (token_id, api_key_id, endpoint, success, latency_ms) VALUES (?, ?, ?, ?, ?)",
(token_id, api_key_id, endpoint, success, latency_ms),
)
conn.commit()
finally:
conn.close()
def get_usage_stats(token_id=None):
"""获取用量统计"""
conn = get_conn()
try:
now = datetime.now(timezone.utc)
today = now.strftime("%Y-%m-%d")
month = now.strftime("%Y-%m")
hour_ago = now.replace(minute=0, second=0, microsecond=0).isoformat()
where = ""
params = []
if token_id:
where = "AND token_id = ?"
params = [token_id]
def count(condition, extra_params=None):
p = (extra_params or []) + list(params)
row = conn.execute(
f"SELECT COUNT(*) as c FROM usage_logs WHERE {condition} {where}", p
).fetchone()
return row["c"]
return {
"today_success": count("success = 1 AND created_at >= ?", [today]),
"today_failed": count("success = 0 AND created_at >= ?", [today]),
"month_success": count("success = 1 AND created_at >= ?", [month]),
"hour_count": count("created_at >= ?", [hour_ago]),
"today_count": count("created_at >= ?", [today]),
"month_count": count("created_at >= ?", [month]),
}
finally:
conn.close()
def check_quota(token_id, hourly_limit, daily_limit, monthly_limit):
"""检查 token 配额是否超限,返回 (ok, reason)"""
stats = get_usage_stats(token_id)
if hourly_limit and stats["hour_count"] >= hourly_limit:
return False, "hourly quota exceeded"
if daily_limit and stats["today_count"] >= daily_limit:
return False, "daily quota exceeded"
if monthly_limit and stats["month_count"] >= monthly_limit:
return False, "monthly quota exceeded"
return True, ""
+10
View File
@@ -0,0 +1,10 @@
services:
tavily-proxy:
build: .
ports:
- "9874:9874"
environment:
- ADMIN_PASSWORD=change-me
volumes:
- ./data:/app/data
restart: unless-stopped
+44
View File
@@ -0,0 +1,44 @@
"""
API Key 轮询池
"""
import threading
from database import get_active_keys, update_key_usage
class KeyPool:
def __init__(self):
self._lock = threading.Lock()
self._index = 0
self._keys = []
self._initialized = False
def reload(self):
with self._lock:
self._keys = [dict(row) for row in get_active_keys()]
if self._index >= len(self._keys):
self._index = 0
self._initialized = True
def get_next_key(self):
"""Round-robin 返回下一个可用 key,返回 dict 或 None"""
if not self._initialized:
self.reload()
with self._lock:
if not self._keys:
return None
key = self._keys[self._index]
self._index = (self._index + 1) % len(self._keys)
return key
def report_result(self, key_id, success):
"""记录使用结果,失败 3 次自动禁用并从池中移除"""
update_key_usage(key_id, success)
if not success:
with self._lock:
self._keys = [k for k in self._keys if k["id"] != key_id or k.get("consecutive_fails", 0) < 2]
# 完整重载以获取最新状态
self.reload()
# 全局单例
pool = KeyPool()
+4
View File
@@ -0,0 +1,4 @@
fastapi>=0.104.0
uvicorn>=0.24.0
httpx>=0.25.0
jinja2>=3.1.0
+199
View File
@@ -0,0 +1,199 @@
"""
Tavily API Proxy — FastAPI 主服务
"""
import os
import time
import httpx
from fastapi import FastAPI, Request, HTTPException, Depends
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
import database as db
from key_pool import pool
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "admin")
TAVILY_API_BASE = "https://api.tavily.com"
app = FastAPI(title="Tavily API Proxy")
templates = Jinja2Templates(directory=os.path.join(os.path.dirname(__file__), "templates"))
http_client = httpx.AsyncClient(timeout=60)
def get_admin_password():
return db.get_setting("admin_password", ADMIN_PASSWORD)
# ═══ Auth helpers ═══
def verify_admin(request: Request):
auth = request.headers.get("Authorization", "")
password = request.headers.get("X-Admin-Password", "")
pwd = get_admin_password()
if auth == f"Bearer {pwd}" or password == pwd:
return True
raise HTTPException(status_code=401, detail="Unauthorized")
def extract_token(request: Request, body: dict = None):
"""从请求中提取用户 token"""
# 1. Authorization header
auth = request.headers.get("Authorization", "")
if auth.startswith("Bearer "):
return auth[7:]
# 2. body 中的 api_key 字段
if body and body.get("api_key"):
return body["api_key"]
return None
# ═══ 启动 ═══
@app.on_event("startup")
def startup():
db.init_db()
# ═══ 代理端点 ═══
@app.post("/api/search")
@app.post("/api/extract")
async def proxy_tavily(request: Request):
body = await request.json()
endpoint = request.url.path.replace("/api/", "") # search or extract
# 验证 token
token_value = extract_token(request, body)
if not token_value:
raise HTTPException(status_code=401, detail="Missing API token")
token_row = db.get_token_by_value(token_value)
if not token_row:
raise HTTPException(status_code=401, detail="Invalid token")
# 检查配额
ok, reason = db.check_quota(
token_row["id"],
token_row["hourly_limit"],
token_row["daily_limit"],
token_row["monthly_limit"],
)
if not ok:
raise HTTPException(status_code=429, detail=reason)
# 从 key pool 取 key
key_info = pool.get_next_key()
if not key_info:
raise HTTPException(status_code=503, detail="No available API keys")
# 替换 api_key 并转发
body["api_key"] = key_info["key"]
start = time.time()
try:
resp = await http_client.post(f"{TAVILY_API_BASE}/{endpoint}", json=body)
latency = int((time.time() - start) * 1000)
success = resp.status_code == 200
# 记录结果
pool.report_result(key_info["id"], success)
db.log_usage(token_row["id"], key_info["id"], endpoint, int(success), latency)
return JSONResponse(content=resp.json(), status_code=resp.status_code)
except Exception as e:
latency = int((time.time() - start) * 1000)
pool.report_result(key_info["id"], False)
db.log_usage(token_row["id"], key_info["id"], endpoint, 0, latency)
raise HTTPException(status_code=502, detail=str(e))
# ═══ 控制台 ═══
@app.get("/", response_class=HTMLResponse)
async def console(request: Request):
return templates.TemplateResponse("console.html", {"request": request})
# ═══ 管理 API ═══
@app.get("/api/stats")
async def stats(request: Request, _=Depends(verify_admin)):
all_stats = db.get_usage_stats()
tokens = [dict(t) for t in db.get_all_tokens()]
for t in tokens:
t["stats"] = db.get_usage_stats(t["id"])
keys = db.get_all_keys()
return {
"overview": all_stats,
"tokens": tokens,
"keys_total": len(keys),
"keys_active": sum(1 for k in keys if k["active"]),
}
@app.get("/api/keys")
async def list_keys(request: Request, _=Depends(verify_admin)):
keys = [dict(k) for k in db.get_all_keys()]
for k in keys:
# 脱敏显示
raw = k["key"]
k["key_masked"] = raw[:8] + "***" + raw[-4:] if len(raw) > 12 else raw
return {"keys": keys}
@app.post("/api/keys")
async def add_keys(request: Request, _=Depends(verify_admin)):
body = await request.json()
if "file" in body:
count = db.import_keys_from_text(body["file"])
pool.reload()
return {"imported": count}
elif "key" in body:
db.add_key(body["key"], body.get("email", ""))
pool.reload()
return {"ok": True}
raise HTTPException(status_code=400, detail="Provide 'key' or 'file'")
@app.delete("/api/keys/{key_id}")
async def remove_key(key_id: int, _=Depends(verify_admin)):
db.delete_key(key_id)
pool.reload()
return {"ok": True}
@app.put("/api/keys/{key_id}/toggle")
async def toggle_key(key_id: int, request: Request, _=Depends(verify_admin)):
body = await request.json()
db.toggle_key(key_id, body.get("active", 1))
pool.reload()
return {"ok": True}
@app.get("/api/tokens")
async def list_tokens(request: Request, _=Depends(verify_admin)):
tokens = [dict(t) for t in db.get_all_tokens()]
for t in tokens:
t["stats"] = db.get_usage_stats(t["id"])
return {"tokens": tokens}
@app.post("/api/tokens")
async def create_token(request: Request, _=Depends(verify_admin)):
body = await request.json()
token = db.create_token(body.get("name", ""))
return {"token": dict(token)}
@app.delete("/api/tokens/{token_id}")
async def remove_token(token_id: int, _=Depends(verify_admin)):
db.delete_token(token_id)
return {"ok": True}
@app.put("/api/password")
async def change_password(request: Request, _=Depends(verify_admin)):
body = await request.json()
new_pwd = body.get("password", "").strip()
if not new_pwd or len(new_pwd) < 4:
raise HTTPException(status_code=400, detail="Password too short (min 4)")
db.set_setting("admin_password", new_pwd)
return {"ok": True}
+383
View File
@@ -0,0 +1,383 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tavily API Proxy</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f0f2f5; color: #1a1a2e; min-height: 100vh; }
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; }
.header-left h1 { font-size: 24px; margin-bottom: 4px; }
.header-left .subtitle { color: #666; font-size: 14px; }
.user-menu { position: relative; }
.user-btn { display: flex; align-items: center; gap: 8px; padding: 8px 16px; background: #fff; border: 1px solid #ddd; border-radius: 8px; cursor: pointer; font-size: 14px; }
.user-btn:hover { background: #f5f5f5; }
.user-btn svg { width: 20px; height: 20px; }
.dropdown { display: none; position: absolute; right: 0; top: 44px; background: #fff; border: 1px solid #e0e0e0; border-radius: 10px; box-shadow: 0 4px 16px rgba(0,0,0,.12); width: 280px; z-index: 100; padding: 16px; }
.dropdown.show { display: block; }
.dropdown h3 { font-size: 14px; margin-bottom: 12px; color: #333; }
.dropdown .pwd-row { display: flex; gap: 8px; margin-bottom: 12px; }
.dropdown .pwd-row input { flex: 1; padding: 6px 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 13px; }
.dropdown .pwd-row button { white-space: nowrap; }
.dropdown-divider { height: 1px; background: #eee; margin: 12px 0; }
.dropdown .logout-btn { display: block; width: 100%; text-align: center; padding: 8px; color: #e74c3c; border: 1px solid #e74c3c; border-radius: 6px; cursor: pointer; font-size: 13px; background: #fff; }
.dropdown .logout-btn:hover { background: #fdf0ef; }
.card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
.card h2 { font-size: 18px; margin-bottom: 6px; }
.card .desc { color: #888; font-size: 13px; margin-bottom: 16px; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
.stat-box { border: 1px solid #e8e8e8; border-radius: 8px; padding: 14px; }
.stat-box .label { font-size: 13px; color: #888; margin-bottom: 4px; }
.stat-box .value { font-size: 28px; font-weight: 700; }
.stat-box .value span { font-size: 16px; color: #888; font-weight: 400; }
.stat-box .remain { font-size: 13px; color: #1a8a1a; margin-top: 4px; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th { text-align: left; padding: 10px 12px; color: #888; font-weight: 500; border-bottom: 1px solid #eee; }
td { padding: 10px 12px; border-bottom: 1px solid #f5f5f5; }
.btn { display: inline-block; padding: 6px 16px; border-radius: 6px; border: 1px solid #ddd; background: #fff; cursor: pointer; font-size: 13px; }
.btn:hover { background: #f5f5f5; }
.btn-primary { background: #1a1a6e; color: #fff; border-color: #1a1a6e; }
.btn-primary:hover { background: #2a2a8e; }
.btn-danger { color: #e74c3c; border-color: #e74c3c; }
.btn-danger:hover { background: #fdf0ef; }
.btn-sm { padding: 4px 10px; font-size: 12px; }
input[type="text"], input[type="password"], textarea { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
textarea { min-height: 80px; resize: vertical; }
.flex { display: flex; gap: 8px; align-items: center; }
.mt { margin-top: 12px; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; }
.tag-ok { background: #e8f5e9; color: #2e7d32; }
.tag-off { background: #fce4ec; color: #c62828; }
.mono { font-family: 'SF Mono', Monaco, Consolas, monospace; font-size: 13px; }
.quota-bar { height: 4px; background: #e8e8e8; border-radius: 2px; margin-top: 3px; }
.quota-bar-fill { height: 100%; border-radius: 2px; background: #1a8a1a; transition: width .3s; }
.quota-bar-fill.warn { background: #f39c12; }
.quota-bar-fill.danger { background: #e74c3c; }
#login-box { max-width: 360px; margin: 120px auto; }
.hidden { display: none; }
.code-block { background: #1a1a2e; color: #e0e0e0; border-radius: 8px; padding: 16px; font-family: 'SF Mono', Monaco, Consolas, monospace; font-size: 13px; line-height: 1.6; overflow-x: auto; position: relative; }
.code-block .comment { color: #6a9955; }
.code-block .string { color: #ce9178; }
.code-block .key { color: #9cdcfe; }
.copy-code-btn { position: absolute; top: 8px; right: 8px; padding: 4px 10px; font-size: 11px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); color: #ccc; border-radius: 4px; cursor: pointer; }
.copy-code-btn:hover { background: rgba(255,255,255,.2); }
</style>
</head>
<body>
<!-- Login -->
<div id="login-box" class="card">
<h2>Tavily API Proxy</h2>
<p class="desc">输入管理密码进入控制台</p>
<input type="password" id="pwd-input" placeholder="密码" onkeydown="if(event.key==='Enter')doLogin()">
<button class="btn btn-primary mt" style="width:100%" onclick="doLogin()">登录</button>
<p id="login-err" style="color:#e74c3c;font-size:13px;margin-top:8px" class="hidden"></p>
</div>
<!-- Dashboard -->
<div id="dashboard" class="hidden">
<div class="container">
<div class="header">
<div class="header-left">
<h1>控制台</h1>
<p class="subtitle">Tavily API Proxy · Key 池管理</p>
</div>
<div class="user-menu">
<div class="user-btn" onclick="toggleDropdown()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M4 21v-1a6 6 0 0112 0v1"/></svg>
Admin
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><path d="M6 9l6 6 6-6"/></svg>
</div>
<div class="dropdown" id="user-dropdown">
<h3>修改密码</h3>
<div class="pwd-row">
<input type="password" id="new-pwd" placeholder="输入新密码">
<button class="btn btn-primary btn-sm" onclick="changePwd()">确认</button>
</div>
<div class="dropdown-divider"></div>
<button class="logout-btn" onclick="logout()">退出登录</button>
</div>
</div>
</div>
<!-- Overview -->
<div class="card">
<h2>用量概览</h2>
<p class="desc">账户级成功率、失败量与总余额</p>
<div class="stats-grid" id="overview-stats"></div>
</div>
<!-- API Usage -->
<div class="card">
<h2>调用方式</h2>
<p class="desc">兼容 Tavily 官方 API,替换 base URL 和 api_key 即可</p>
<div style="margin-bottom:12px">
<div style="font-size:13px;color:#666;margin-bottom:6px">API 地址</div>
<div class="mono" style="font-size:14px;padding:8px 12px;background:#f5f5f5;border-radius:6px;display:inline-block" id="api-base-url"></div>
</div>
<div style="font-size:13px;color:#666;margin-bottom:6px">调用示例 (curl)</div>
<div class="code-block" id="curl-example">
<button class="copy-code-btn" onclick="copyCurl()">复制</button>
</div>
</div>
<!-- Tokens -->
<div class="card">
<h2>Token 列表</h2>
<p class="desc">代理访问 Token,分配给应用使用</p>
<div class="flex mt" style="margin-bottom:12px">
<input type="text" id="token-name" placeholder="Token 备注名(可选)" style="max-width:240px">
<button class="btn btn-primary" onclick="createToken()">创建 Token</button>
</div>
<div style="overflow-x:auto">
<table id="tokens-table"><thead><tr><th>Token</th><th>备注</th><th>配额 / 余额</th><th>用量统计</th><th>操作</th></tr></thead><tbody></tbody></table>
</div>
</div>
<!-- Keys -->
<div class="card">
<h2>API Key 池</h2>
<p class="desc">管理 Tavily API Key,支持批量导入</p>
<div class="flex mt" style="margin-bottom:12px;flex-wrap:wrap">
<input type="text" id="single-key" placeholder="tvly-xxx" style="max-width:360px">
<button class="btn btn-primary" onclick="addSingleKey()">添加</button>
<button class="btn" onclick="document.getElementById('import-area').classList.toggle('hidden')">批量导入</button>
</div>
<div id="import-area" class="hidden" style="margin-bottom:12px">
<textarea id="import-text" placeholder="粘贴 api_keys.md 内容,每行一条&#10;格式: email,password,tvly-xxx,timestamp;"></textarea>
<button class="btn btn-primary mt" onclick="importKeys()">导入</button>
</div>
<div style="overflow-x:auto">
<table id="keys-table"><thead><tr><th>ID</th><th>Key</th><th>邮箱</th><th>成功</th><th>失败</th><th>状态</th><th>操作</th></tr></thead><tbody></tbody></table>
</div>
</div>
</div>
</div>
<script>
const API = '';
let PWD = localStorage.getItem('tavily_proxy_pwd') || '';
function headers() {
return { 'Content-Type': 'application/json', 'X-Admin-Password': PWD };
}
async function api(method, path, body) {
const opts = { method, headers: headers() };
if (body) opts.body = JSON.stringify(body);
const r = await fetch(API + path, opts);
if (r.status === 401) { logout(); throw new Error('Unauthorized'); }
return r.json();
}
function doLogin() {
PWD = document.getElementById('pwd-input').value;
localStorage.setItem('tavily_proxy_pwd', PWD);
api('GET', '/api/stats').then(() => {
document.getElementById('login-box').classList.add('hidden');
document.getElementById('dashboard').classList.remove('hidden');
refresh();
}).catch(() => {
document.getElementById('login-err').textContent = '密码错误';
document.getElementById('login-err').classList.remove('hidden');
});
}
function logout() {
PWD = '';
localStorage.removeItem('tavily_proxy_pwd');
document.getElementById('login-box').classList.remove('hidden');
document.getElementById('dashboard').classList.add('hidden');
document.getElementById('user-dropdown').classList.remove('show');
}
function toggleDropdown() {
document.getElementById('user-dropdown').classList.toggle('show');
}
// Close dropdown on outside click
document.addEventListener('click', e => {
const menu = document.querySelector('.user-menu');
if (menu && !menu.contains(e.target)) {
document.getElementById('user-dropdown').classList.remove('show');
}
});
function quotaBar(used, limit) {
const pct = limit ? Math.min(100, (used / limit) * 100) : 0;
const cls = pct >= 90 ? 'danger' : pct >= 70 ? 'warn' : '';
return `<div class="quota-bar"><div class="quota-bar-fill ${cls}" style="width:${pct}%"></div></div>`;
}
async function refresh() {
const stats = await api('GET', '/api/stats');
const o = stats.overview;
// 每个 Tavily 免费 key 月限额 1000 次
const KEY_MONTHLY_QUOTA = 1000;
const totalQuota = stats.keys_active * KEY_MONTHLY_QUOTA;
const monthUsed = o.month_count;
const monthRemain = Math.max(0, totalQuota - monthUsed);
const usedPct = totalQuota ? Math.min(100, (monthUsed / totalQuota) * 100) : 0;
const remainCls = usedPct >= 90 ? 'color:#e74c3c' : usedPct >= 70 ? 'color:#f39c12' : 'color:#1a8a1a';
document.getElementById('overview-stats').innerHTML = `
<div class="stat-box"><div class="label">本月总额度</div><div class="value">${totalQuota.toLocaleString()}</div><div class="remain">Key ${stats.keys_active} × ${KEY_MONTHLY_QUOTA}</div></div>
<div class="stat-box"><div class="label">已用次数</div><div class="value">${monthUsed}</div><div class="remain">成功 ${o.month_success} / 失败 ${monthUsed - o.month_success}</div></div>
<div class="stat-box"><div class="label">剩余次数</div><div class="value" style="${remainCls}">${monthRemain.toLocaleString()}</div>${quotaBar(monthUsed, totalQuota)}</div>
<div class="stat-box"><div class="label">今日用量</div><div class="value">${o.today_count}</div><div class="remain">成功 ${o.today_success} / 失败 ${o.today_failed}</div></div>
<div class="stat-box"><div class="label">Key 池</div><div class="value">${stats.keys_active} <span>/ ${stats.keys_total}</span></div></div>
<div class="stat-box"><div class="label">Token 数</div><div class="value">${stats.tokens.length}</div></div>
`;
// API usage info
const baseUrl = location.origin;
document.getElementById('api-base-url').textContent = baseUrl;
const firstToken = stats.tokens.length > 0 ? stats.tokens[0].token : 'YOUR_TOKEN';
document.getElementById('curl-example').innerHTML = `<button class="copy-code-btn" onclick="copyCurl()">复制</button><span class="comment"># Search 搜索</span>
curl -X POST <span class="string">${baseUrl}/api/search</span> \\
-H <span class="string">"Content-Type: application/json"</span> \\
-H <span class="string">"Authorization: Bearer <span class="key">${firstToken}</span>"</span> \\
-d <span class="string">'{"query": "your search query"}'</span>
<span class="comment"># Extract 内容提取</span>
curl -X POST <span class="string">${baseUrl}/api/extract</span> \\
-H <span class="string">"Content-Type: application/json"</span> \\
-H <span class="string">"Authorization: Bearer <span class="key">${firstToken}</span>"</span> \\
-d <span class="string">'{"urls": ["https://example.com"]}'</span>
<span class="comment"># 或在 body 中传 api_key</span>
curl -X POST <span class="string">${baseUrl}/api/search</span> \\
-H <span class="string">"Content-Type: application/json"</span> \\
-d <span class="string">'{"api_key": "<span class="key">${firstToken}</span>", "query": "your search query"}'</span>`;
// Tokens
const tokensData = await api('GET', '/api/tokens');
const tbody = document.querySelector('#tokens-table tbody');
tbody.innerHTML = '';
for (const t of tokensData.tokens) {
const s = t.stats || {};
const unlimited = !t.hourly_limit && !t.daily_limit && !t.monthly_limit;
let quotaHtml;
if (unlimited) {
quotaHtml = '<span style="color:#1a8a1a;font-weight:600">无限制</span>';
} else {
const hr = t.hourly_limit - (s.hour_count||0);
const dr = t.daily_limit - (s.today_count||0);
const mr = t.monthly_limit - (s.month_count||0);
quotaHtml = `
<div>小时 ${s.hour_count||0}/${t.hourly_limit} <b style="color:${hr>0?'#1a8a1a':'#e74c3c'}">余 ${hr}</b>${quotaBar(s.hour_count||0, t.hourly_limit)}</div>
<div>今日 ${s.today_count||0}/${t.daily_limit} <b style="color:${dr>0?'#1a8a1a':'#e74c3c'}">余 ${dr}</b>${quotaBar(s.today_count||0, t.daily_limit)}</div>
<div>本月 ${s.month_count||0}/${t.monthly_limit} <b style="color:${mr>0?'#1a8a1a':'#e74c3c'}">余 ${mr}</b>${quotaBar(s.month_count||0, t.monthly_limit)}</div>`;
}
tbody.innerHTML += `<tr>
<td class="mono">${t.token}</td>
<td style="font-size:12px">${t.name||'-'}</td>
<td style="font-size:12px;line-height:2">${quotaHtml}</td>
<td style="font-size:12px;line-height:1.8">
成功 ${s.today_success||0} / 失败 ${s.today_failed||0}<br>
本月 ${s.month_success||0}
</td>
<td>
<button class="btn btn-sm" onclick="copyText('${t.token}')">复制</button>
<button class="btn btn-sm btn-danger" onclick="delToken(${t.id})">删除</button>
</td>
</tr>`;
}
// Keys
const keysData = await api('GET', '/api/keys');
const ktbody = document.querySelector('#keys-table tbody');
ktbody.innerHTML = '';
for (const k of keysData.keys) {
const active = k.active === 1;
ktbody.innerHTML += `<tr>
<td>${k.id}</td>
<td class="mono">${k.key_masked}</td>
<td style="font-size:12px">${k.email||'-'}</td>
<td>${k.total_used}</td>
<td>${k.total_failed}</td>
<td><span class="tag ${active?'tag-ok':'tag-off'}">${active?'正常':'禁用'}</span></td>
<td>
<button class="btn btn-sm" onclick="toggleKey(${k.id},${active?0:1})">${active?'禁用':'启用'}</button>
<button class="btn btn-sm btn-danger" onclick="delKey(${k.id})">删除</button>
</td>
</tr>`;
}
}
async function createToken() {
const name = document.getElementById('token-name').value;
await api('POST', '/api/tokens', { name });
document.getElementById('token-name').value = '';
refresh();
}
async function delToken(id) { if(confirm('确认删除?')){ await api('DELETE', `/api/tokens/${id}`); refresh(); } }
async function addSingleKey() {
const key = document.getElementById('single-key').value.trim();
if (!key) return;
await api('POST', '/api/keys', { key });
document.getElementById('single-key').value = '';
refresh();
}
async function importKeys() {
const text = document.getElementById('import-text').value;
if (!text.trim()) return;
const r = await api('POST', '/api/keys', { file: text });
alert(`已导入 ${r.imported} 个 Key`);
document.getElementById('import-text').value = '';
document.getElementById('import-area').classList.add('hidden');
refresh();
}
async function delKey(id) { if(confirm('确认删除?')){ await api('DELETE', `/api/keys/${id}`); refresh(); } }
async function toggleKey(id, active) { await api('PUT', `/api/keys/${id}/toggle`, { active }); refresh(); }
async function changePwd() {
const newPwd = document.getElementById('new-pwd').value.trim();
if (!newPwd || newPwd.length < 4) { alert('密码至少 4 位'); return; }
try {
await api('PUT', '/api/password', { password: newPwd });
PWD = newPwd;
localStorage.setItem('tavily_proxy_pwd', PWD);
document.getElementById('new-pwd').value = '';
document.getElementById('user-dropdown').classList.remove('show');
alert('密码已修改');
} catch(e) { alert('修改失败: ' + e.message); }
}
function copyCurl() {
const el = document.getElementById('curl-example');
const text = el.innerText.replace('复制', '').trim();
navigator.clipboard.writeText(text).then(() => {
const btn = el.querySelector('.copy-code-btn');
btn.textContent = '已复制';
setTimeout(() => btn.textContent = '复制', 1500);
});
}
function copyText(t) {
navigator.clipboard.writeText(t).then(() => {
const el = event.target;
el.textContent = '已复制';
setTimeout(() => el.textContent = '复制', 1500);
});
}
// Auto login
if (PWD) {
api('GET', '/api/stats').then(() => {
document.getElementById('login-box').classList.add('hidden');
document.getElementById('dashboard').classList.remove('hidden');
refresh();
}).catch(() => {});
}
</script>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
camoufox==0.4.11
curl_cffi==0.14.0
patchright==1.58.2
quart==0.20.0
requests==2.32.5
rich==14.3.3
playwright==1.49.1
Executable
+290
View File
@@ -0,0 +1,290 @@
#!/usr/bin/env python3
import os
import sys
import subprocess
# ──────────────────────────────────────────────
# 启动前自动检查并安装依赖
# ──────────────────────────────────────────────
def _ensure_venv():
"""确保虚拟环境存在并激活"""
_HERE = os.path.dirname(os.path.abspath(__file__))
venv_dir = os.path.join(_HERE, "venv")
# 如果已经在虚拟环境中,直接返回
if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
return
# 创建虚拟环境
if not os.path.exists(venv_dir):
print("创建虚拟环境...")
subprocess.check_call([sys.executable, "-m", "venv", venv_dir])
print("✅ 虚拟环境创建完成\n")
# 重新启动脚本在虚拟环境中
if sys.platform == "win32":
python_exe = os.path.join(venv_dir, "Scripts", "python.exe")
else:
python_exe = os.path.join(venv_dir, "bin", "python")
os.execv(python_exe, [python_exe] + sys.argv)
def _ensure_deps():
_HERE = os.path.dirname(os.path.abspath(__file__))
req_file = os.path.join(_HERE, "requirements.txt")
missing = []
pkg_map = {
"camoufox": "camoufox",
"curl_cffi": "curl_cffi",
"patchright": "patchright",
"quart": "quart",
"requests": "requests",
"rich": "rich",
"playwright": "playwright",
}
for mod, pkg in pkg_map.items():
try:
__import__(mod)
except ImportError:
missing.append(pkg)
if missing:
print(f"正在安装依赖: {', '.join(missing)}...")
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", req_file, "-q"])
print("✅ 依赖安装完成\n")
# 检查 camoufox 浏览器
try:
import camoufox
data_dir = os.path.join(os.path.dirname(camoufox.__file__), "data")
if not os.path.exists(data_dir) or not os.listdir(data_dir):
print("正在下载 Camoufox 浏览器...")
subprocess.check_call([sys.executable, "-m", "camoufox", "fetch"])
print("✅ 浏览器下载完成\n")
except Exception:
pass
# 安装 Playwright 浏览器
try:
import playwright
pw_browsers = os.path.join(os.path.dirname(playwright.__file__), "driver", "package", ".local-browsers")
if not os.path.exists(pw_browsers):
print("正在安装 Playwright 浏览器...")
subprocess.check_call([sys.executable, "-m", "playwright", "install", "chromium"])
print("✅ Playwright 浏览器安装完成\n")
except Exception:
pass
_ensure_venv()
_ensure_deps()
import time
import signal
import requests as std_requests
from config import SERVER_URL, SERVER_ADMIN_PASSWORD, DEFAULT_COUNT, DEFAULT_DELAY
from tavily_core import create_email, register
# ──────────────────────────────────────────────
# Solver 管理
# ──────────────────────────────────────────────
solver_proc = None
def start_solver():
global solver_proc
# 清理旧进程
try:
import psutil
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
cmdline = proc.info.get('cmdline') or []
if any('api_solver.py' in str(c) for c in cmdline):
print(f"清理旧 Solver 进程 (PID: {proc.pid})")
proc.kill()
time.sleep(1)
except:
pass
except ImportError:
# 没有 psutil,用 lsof 检查端口
import subprocess
try:
result = subprocess.run(['lsof', '-ti', ':5072'], capture_output=True, text=True)
if result.stdout.strip():
pid = result.stdout.strip()
subprocess.run(['kill', '-9', pid])
time.sleep(1)
except:
pass
# 启动 Solver
print("启动 Turnstile Solver...")
# 获取 Python 路径
if os.path.exists('venv'):
if sys.platform == 'win32':
python_path = os.path.join('venv', 'Scripts', 'python.exe')
else:
python_path = os.path.join('venv', 'bin', 'python3')
else:
python_path = sys.executable
solver_proc = subprocess.Popen(
[python_path, 'api_solver.py', '--browser_type', 'chromium', '--thread', '1'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
# 等待启动
for i in range(30):
try:
r = std_requests.get("http://127.0.0.1:5072/", timeout=1)
if r.status_code == 200:
print("✅ Solver 已启动\n")
return True
except:
pass
time.sleep(1)
if i % 5 == 0:
print(f"等待 Solver 启动... ({i}s)")
print("❌ Solver 启动超时")
return False
def stop_solver():
global solver_proc
if solver_proc:
solver_proc.terminate()
solver_proc.wait()
solver_proc = None
def signal_handler(sig, frame):
print("\n\n正在退出...")
stop_solver()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
# ──────────────────────────────────────────────
# 上传到代理服务器
# ──────────────────────────────────────────────
def upload_key(email, api_key):
try:
r = std_requests.post(
f"{SERVER_URL}/api/keys",
json={"key": api_key, "email": email},
headers={"Authorization": f"Bearer {SERVER_ADMIN_PASSWORD}"},
timeout=15,
)
if r.status_code in (200, 201):
print("✅ 已上传服务器")
return True
print(f"⚠️ 上传失败 {r.status_code}: {r.text[:100]}")
return False
except Exception as e:
print(f"⚠️ 上传失败: {e}")
return False
# ──────────────────────────────────────────────
# 注册流程
# ──────────────────────────────────────────────
def do_register(count, delay, upload, provider="cloudflare"):
success = 0
failed = 0
for i in range(count):
if i > 0:
print(f"\n⏳ 等待 {delay} 秒...\n")
time.sleep(delay)
print(f"{'='*60}")
print(f"📧 注册 ({i+1}/{count})")
print(f"{'='*60}\n")
try:
email, password, actual_provider = create_email(provider)
result = register(email, password, actual_provider)
if result and result != "SUCCESS_NO_KEY":
success += 1
if upload:
upload_key(email, result)
elif result == "SUCCESS_NO_KEY":
success += 1
else:
failed += 1
except Exception as e:
print(f"❌ 注册异常: {e}")
failed += 1
print(f"\n{'='*60}")
print(f"✅ 成功: {success} ❌ 失败: {failed}")
print(f"{'='*60}\n")
# ──────────────────────────────────────────────
# 交互菜单
# ──────────────────────────────────────────────
def menu_register():
print(f"\n 邮箱服务商:")
print(f" 1. Cloudflare Email Workers (nashome.me)")
print(f" 2. DuckMail")
print(f" 选择 [1]: ", end="")
provider_input = input().strip()
provider = "duckmail" if provider_input == "2" else "cloudflare"
print(f"\n 注册数量 (默认 {DEFAULT_COUNT}): ", end="")
count_input = input().strip()
count = int(count_input) if count_input.isdigit() else DEFAULT_COUNT
print(f" 间隔秒数 (默认 {DEFAULT_DELAY}): ", end="")
delay_input = input().strip()
delay = int(delay_input) if delay_input.isdigit() else DEFAULT_DELAY
print(f" 上传服务器? [Y/n]: ", end="")
upload_input = input().strip().lower()
upload = upload_input not in ("n", "no")
provider_name = "DuckMail" if provider == "duckmail" else "Cloudflare"
print(f"\n 邮箱: {provider_name} 数量: {count} 间隔: {delay}s 上传: {'是' if upload else '否'}")
do_register(count, delay, upload, provider)
def main():
# 启动 Solver
if not start_solver():
print("无法启动 Solver,退出")
return
try:
while True:
print("""
┌──────────────────────────────────────────┐
│ Tavily 全自动注册工具 │
├──────────────────────────────────────────┤
│ 注册无需邮件验证,邮箱地址随机生成 │
│ 账号/密码/API Key 保存至 accounts.txt │
├──────────────────────────────────────────┤
│ 1. 开始注册 │
│ 0. 退出 │
└──────────────────────────────────────────┘
选择: """, end="")
choice = input().strip()
if choice == "1":
menu_register()
elif choice == "0":
break
else:
print("无效选择")
finally:
stop_solver()
if __name__ == "__main__":
main()
+522
View File
@@ -0,0 +1,522 @@
"""
Tavily 注册核心模块 - 简化版
"""
import os
import re
import time
import random
import string
import requests as std_requests
from curl_cffi import requests
from camoufox.sync_api import Camoufox
from config import EMAIL_API_URL, EMAIL_API_TOKEN, EMAIL_DOMAIN, DUCKMAIL_API_URL, DUCKMAIL_API_KEY, FIXED_PASSWORD
# ──────────────────────────────────────────────
# 配置
# ──────────────────────────────────────────────
LOCAL_SOLVER_URL = "http://127.0.0.1:5072"
TURNSTILE_SITEKEY = "0x4AAAAAAAQFNSW6xordsuIq"
_HERE = os.path.dirname(os.path.abspath(__file__))
_SAVE_FILE = os.path.join(_HERE, "accounts.txt")
def rand_str(n=8):
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=n))
def _save_account(email, password, api_key=None):
with open(_SAVE_FILE, 'a', encoding='utf-8') as f:
f.write(f"{email},{password},{api_key or ''}\n")
# ──────────────────────────────────────────────
# 邮箱相关
# ──────────────────────────────────────────────
def create_email(provider="cloudflare"):
"""生成随机邮箱地址
Args:
provider: "cloudflare" 或 "duckmail"
"""
username = f"tavily-{rand_str()}"
password = FIXED_PASSWORD # 使用固定密码
if provider == "duckmail":
# 获取 DuckMail 可用域名
try:
r = std_requests.get(
f"{DUCKMAIL_API_URL}/domains",
headers={"Authorization": f"Bearer {DUCKMAIL_API_KEY}"},
timeout=10
)
if r.status_code == 200:
domains = r.json()
if domains and len(domains) > 0:
domain = domains[0].get('domain', 'duckmail.sbs')
else:
domain = 'duckmail.sbs'
else:
domain = 'duckmail.sbs'
except:
domain = 'duckmail.sbs'
email = f"{username}@{domain}"
# 创建 DuckMail 账户
try:
r = std_requests.post(
f"{DUCKMAIL_API_URL}/accounts",
json={"address": email, "password": password},
headers={"Authorization": f"Bearer {DUCKMAIL_API_KEY}"},
timeout=10
)
if r.status_code == 201:
print(f"✅ 邮箱: {email}")
return email, password, provider
else:
print(f"⚠️ DuckMail 创建失败: {r.status_code}, 使用 Cloudflare")
return create_email("cloudflare")
except Exception as e:
print(f"⚠️ DuckMail 错误: {e}, 使用 Cloudflare")
return create_email("cloudflare")
# Cloudflare Email Workers(默认)
email = f"{username}@{EMAIL_DOMAIN}"
print(f"✅ 邮箱: {email}")
return email, password, "cloudflare"
def get_verification_link(email, password, provider, timeout=120):
"""等待验证邮件并提取验证链接
Args:
email: 邮箱地址
password: 邮箱密码
provider: "cloudflare" 或 "duckmail"
timeout: 超时时间(秒)
"""
print(f"⏳ 等待验证邮件(最多 {timeout} 秒)...", end="", flush=True)
start_time = time.time()
token = None
# DuckMail 需要先获取 token
if provider == "duckmail":
try:
r = std_requests.post(
f"{DUCKMAIL_API_URL}/token",
json={"address": email, "password": password},
timeout=10
)
if r.status_code == 200:
token = r.json().get('token')
else:
print(f"\n⚠️ DuckMail 认证失败: {r.status_code}")
return None
except Exception as e:
print(f"\n⚠️ DuckMail 认证错误: {e}")
return None
while time.time() - start_time < timeout:
try:
if provider == "duckmail":
# DuckMail API
r = std_requests.get(
f"{DUCKMAIL_API_URL}/messages",
headers={"Authorization": f"Bearer {token}"},
timeout=10
)
if r.status_code == 200:
messages = r.json()
for msg in messages:
subject = msg.get("subject", "").lower()
if "verify" in subject or "tavily" in subject:
# 获取完整邮件内容
msg_id = msg.get("id")
r_detail = std_requests.get(
f"{DUCKMAIL_API_URL}/messages/{msg_id}",
headers={"Authorization": f"Bearer {token}"},
timeout=10
)
if r_detail.status_code == 200:
detail = r_detail.json()
html = detail.get("html", "")
text = detail.get("text", "")
content = html + " " + text
match = re.search(r'https://[^\s<>"]*verif[^\s<>"]*', content, re.IGNORECASE)
if match:
verify_url = match.group(0)
print(f"\n✅ 找到验证链接")
return verify_url
else:
# Cloudflare Email Workers API
r = std_requests.get(
f"{EMAIL_API_URL}/messages",
params={"address": email},
headers={"Authorization": f"Bearer {EMAIL_API_TOKEN}"},
timeout=10
)
if r.status_code == 200:
messages = r.json().get("messages", [])
for msg in messages:
subject = msg.get("subject", "").lower()
if "verify" in subject or "tavily" in subject.lower():
# 提取验证链接
html = msg.get("html", "")
text = msg.get("text", "")
content = html + " " + text
match = re.search(r'https://[^\s<>"]*verif[^\s<>"]*', content, re.IGNORECASE)
if match:
verify_url = match.group(0)
print(f"\n✅ 找到验证链接")
return verify_url
time.sleep(5)
print(".", end="", flush=True)
except Exception as e:
print(f"\n⚠️ 检查邮件出错: {e}")
time.sleep(5)
print(f"\n❌ 验证邮件超时")
return None
# ──────────────────────────────────────────────
# 浏览器验证
# ──────────────────────────────────────────────
def verify_with_browser(verify_url, session):
"""使用 Camoufox 处理验证链接并获取 API Key"""
print("🌐 启动浏览器验证...")
try:
with Camoufox(headless=True) as browser:
page = browser.new_page()
# 转移 cookies
cookies = []
for name, value in session.cookies.items():
cookies.append({
"name": name,
"value": value,
"domain": ".tavily.com",
"path": "/"
})
# 同时添加 auth.tavily.com 的 cookies
for name, value in session.cookies.items():
cookies.append({
"name": name,
"value": value,
"domain": ".auth.tavily.com",
"path": "/"
})
page.context.add_cookies(cookies)
# 访问验证链接
page.goto(verify_url, wait_until="networkidle", timeout=60000)
# 等待跳转到主页
page.wait_for_url("**/app.tavily.com/**", timeout=60000)
# 等待页面完全加载
time.sleep(5)
# 访问主页获取 API Key
print("🔑 获取 API Key...")
page.goto("https://app.tavily.com/home", wait_until="networkidle")
time.sleep(3)
# 从页面 HTML 中提取 API Key
html = page.content()
# 匹配完整的 API Key 格式
api_key_matches = re.findall(r'tvly-[a-zA-Z0-9_-]{50,}', html)
if api_key_matches:
# 取最长的那个(排除占位符)
api_keys = [k for k in api_key_matches if k != "tvly-YOUR_API_KEY"]
if api_keys:
api_key = max(api_keys, key=len)
browser.close()
print("✅ 浏览器验证完成")
return api_key
browser.close()
print("⚠️ 未找到 API Key")
return None
except Exception as e:
print(f"❌ 浏览器验证失败: {e}")
return None
# ──────────────────────────────────────────────
# Turnstile Solver
# ──────────────────────────────────────────────
def solve_turnstile(url):
"""调用本地 Solver 解析 Turnstile"""
try:
r = std_requests.get(
f"{LOCAL_SOLVER_URL}/turnstile",
params={"url": url, "sitekey": TURNSTILE_SITEKEY},
timeout=10
)
if r.status_code != 200:
print(f"❌ Solver 请求失败: {r.status_code}")
return None
task_id = r.json().get("taskId")
if not task_id:
print("❌ 未获取到 Task ID")
return None
# 轮询结果
for _ in range(60):
time.sleep(2)
res = std_requests.get(
f"{LOCAL_SOLVER_URL}/result",
params={"id": task_id},
timeout=10
)
if res.status_code == 200:
data = res.json()
if data.get("status") == "ready":
token = data.get("solution", {}).get("token")
if token:
return token
elif data.get("status") == "CAPTCHA_FAIL":
print("❌ Turnstile 解析失败")
return None
print("❌ Turnstile 超时")
return None
except Exception as e:
print(f"❌ Solver 异常: {e}")
return None
# ──────────────────────────────────────────────
# Tavily 注册
# ──────────────────────────────────────────────
def register(email, password, provider="cloudflare"):
"""
注册 Tavily 账号
返回: API Key 或 None
"""
s = requests.Session(impersonate="chrome")
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
}
# 1. 访问登录页
r1 = s.get("https://app.tavily.com/sign-in", headers=headers)
if r1.status_code != 200:
print("❌ 无法访问登录页")
return None
# 提取 state
state_m = re.search(r'state=([^"&]+)', r1.text)
if not state_m:
print("❌ 未找到 state")
return None
state = state_m.group(1)
# 2. 第一次 Turnstile
print("🔐 Turnstile (1/2)...")
token1 = solve_turnstile(f"https://auth.tavily.com/u/login/identifier?state={state}")
if not token1:
return None
print("✅ Turnstile OK")
# 3. 提交邮箱
r2 = s.post(
f"https://auth.tavily.com/u/login/identifier?state={state}",
data={
"state": state,
"username": email,
"action": "default",
"captcha": token1
},
headers=headers,
allow_redirects=False
)
if r2.status_code != 302:
print("❌ 邮箱提交失败")
return None
# 检查是否跳转到注册页
loc = r2.headers.get('Location', '')
if '/login/password' in loc:
# 已有账号,切换到注册
r2_text = s.get(f"https://auth.tavily.com{loc}", headers=headers).text
signup_m = re.search(r'href="(/u/signup/identifier[^"]*)"', r2_text)
if not signup_m:
print("❌ 无法切换到注册页")
return None
signup_url = f"https://auth.tavily.com{signup_m.group(1)}"
r2 = s.get(signup_url, headers=headers)
# 重新提取 state
state_m2 = re.search(r'name="state"\s+value="([^"]+)"', r2.text)
if state_m2:
state = state_m2.group(1)
# 第二次 Turnstile
print("🔐 Turnstile (2/2)...")
token2 = solve_turnstile(signup_url)
if not token2:
return None
print("✅ Turnstile OK")
# 提交注册
r3 = s.post(
signup_url,
data={
"state": state,
"email": email,
"action": "default",
"captcha": token2
},
headers=headers,
allow_redirects=True
)
else:
r3 = s.get(f"https://auth.tavily.com{loc}", headers=headers, allow_redirects=True)
# 4. 到达密码页
if '/password' not in r3.url:
print(f"❌ 未到达密码页面: {r3.url}")
return None
# 第三次 Turnstile
print("🔐 Turnstile (3/3)...")
token3 = solve_turnstile(r3.url)
if not token3:
return None
print("✅ Turnstile OK")
# 提取 state 和 email
state_m3 = re.search(r'name="state"\s+value="([^"]+)"', r3.text)
if state_m3:
state = state_m3.group(1)
email_m = re.search(r'name="email"\s+value="([^"]*)"', r3.text)
email_val = email_m.group(1) if email_m else email
# 5. 提交密码
r4 = s.post(
r3.url,
data={
"state": state,
"email": email_val,
"password": password,
"action": "default",
"captcha": token3
},
headers=headers,
allow_redirects=False
)
if r4.status_code != 302:
print(f"❌ 密码提交失败 (状态码: {r4.status_code})")
return None
# 6. 跟随回调
callback = r4.headers.get('Location', '')
if callback.startswith('/'):
callback = f"https://auth.tavily.com{callback}"
r5 = s.get(callback, allow_redirects=True)
if 'app.tavily.com' not in r5.url:
print("❌ 登录失败")
return None
# 7. 检查账号状态
print("检查账号状态...")
r_account = s.get("https://app.tavily.com/api/account")
if r_account.status_code == 403:
account_data = r_account.json()
if account_data.get('code') == 'email_not_verified':
print("📧 需要邮件验证")
verify_url = get_verification_link(email, password, provider, timeout=60)
if not verify_url:
print("❌ 未收到验证邮件")
_save_account(email, password, None)
return None
# 使用浏览器处理验证链接并直接获取 API Key
api_key = verify_with_browser(verify_url, s)
if not api_key:
print("❌ 验证失败或未获取到 API Key")
_save_account(email, password, None)
return None
# 保存账号
_save_account(email, password, api_key)
print(f"🎉 注册成功")
print(f" 邮箱: {email}")
print(f" 密码: {password}")
print(f" Key : {api_key}")
return api_key
# 8. 获取 API Key
api_key = get_api_key(s, email, password)
if api_key:
_save_account(email, password, api_key)
print(f"🎉 注册成功")
print(f" 邮箱: {email}")
print(f" 密码: {password}")
print(f" Key : {api_key}")
return api_key
else:
_save_account(email, password, None)
print("⚠️ 注册成功,未获取到 API Key")
print(f" 邮箱: {email}")
print(f" 密码: {password}")
return "SUCCESS_NO_KEY"
def get_api_key(session, email, password):
"""获取 API Key(不创建新的)"""
time.sleep(3)
session.get("https://app.tavily.com/home")
time.sleep(2)
r_orgs = session.get("https://app.tavily.com/api/organizations")
if r_orgs.status_code != 200:
return None
orgs = r_orgs.json()
if not orgs:
print("⚠️ 没有找到组织")
return None
org_id = orgs[0].get('id') or orgs[0].get('oid')
if not org_id:
return None
# 直接获取已有的 API Keys(不创建新的)
r_keys = session.get(f"https://app.tavily.com/api/keys?oid={org_id}")
if r_keys.status_code == 200:
keys = r_keys.json()
if keys and len(keys) > 0:
return keys[0].get('key')
print("⚠️ 未找到 API Key")
return None
+366
View File
@@ -0,0 +1,366 @@
"""
Tavily 注册核心模块 - 简化版
"""
import os
import re
import time
import random
import string
import requests as std_requests
from curl_cffi import requests
from config import EMAIL_API_URL, EMAIL_API_TOKEN, EMAIL_DOMAIN
# ──────────────────────────────────────────────
# 配置
# ──────────────────────────────────────────────
LOCAL_SOLVER_URL = "http://127.0.0.1:5072"
TURNSTILE_SITEKEY = "0x4AAAAAAAQFNSW6xordsuIq"
_HERE = os.path.dirname(os.path.abspath(__file__))
_SAVE_FILE = os.path.join(_HERE, "accounts.txt")
def rand_str(n=8):
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=n))
def _save_account(email, password, api_key=None):
with open(_SAVE_FILE, 'a', encoding='utf-8') as f:
f.write(f"{email},{password},{api_key or ''}\n")
# ──────────────────────────────────────────────
# 邮箱相关
# ──────────────────────────────────────────────
def create_email():
"""生成随机邮箱地址(无需预创建,自动接收)"""
username = f"tavily-{rand_str()}"
email = f"{username}@{EMAIL_DOMAIN}"
password = f"Tv{rand_str(4)}{random.randint(10,99)}!"
print(f"✅ 邮箱: {email}")
return email, password
def get_verification_link(email, timeout=120):
"""等待验证邮件并提取验证链接"""
print(f"⏳ 等待验证邮件(最多 {timeout} 秒)...", end="", flush=True)
start_time = time.time()
while time.time() - start_time < timeout:
try:
r = std_requests.get(
f"{EMAIL_API_URL}/messages",
params={"address": email},
headers={"Authorization": f"Bearer {EMAIL_API_TOKEN}"},
timeout=10
)
if r.status_code == 200:
messages = r.json().get("messages", [])
for msg in messages:
subject = msg.get("subject", "").lower()
if "verify" in subject or "tavily" in subject.lower():
# 提取验证链接
html = msg.get("html", "")
text = msg.get("text", "")
content = html + " " + text
match = re.search(r'https://[^\s<>"]+verify[^\s<>"]*', content, re.IGNORECASE)
if match:
verify_url = match.group(0)
print(f"\n✅ 找到验证链接")
return verify_url
time.sleep(5)
print(".", end="", flush=True)
except Exception as e:
print(f"\n⚠️ 检查邮件出错: {e}")
time.sleep(5)
print(f"\n❌ 验证邮件超时")
return None
# ──────────────────────────────────────────────
# Turnstile Solver
# ──────────────────────────────────────────────
def solve_turnstile(url):
"""调用本地 Solver 解析 Turnstile"""
try:
r = std_requests.get(
f"{LOCAL_SOLVER_URL}/turnstile",
params={"url": url, "sitekey": TURNSTILE_SITEKEY},
timeout=10
)
if r.status_code != 200:
print(f"❌ Solver 请求失败: {r.status_code}")
return None
task_id = r.json().get("taskId")
if not task_id:
print("❌ 未获取到 Task ID")
return None
# 轮询结果
for _ in range(60):
time.sleep(2)
res = std_requests.get(
f"{LOCAL_SOLVER_URL}/result",
params={"id": task_id},
timeout=10
)
if res.status_code == 200:
data = res.json()
if data.get("status") == "ready":
token = data.get("solution", {}).get("token")
if token:
return token
elif data.get("status") == "CAPTCHA_FAIL":
print("❌ Turnstile 解析失败")
return None
print("❌ Turnstile 超时")
return None
except Exception as e:
print(f"❌ Solver 异常: {e}")
return None
# ──────────────────────────────────────────────
# Tavily 注册
# ──────────────────────────────────────────────
def register(email, password):
"""
注册 Tavily 账号
返回: API Key 或 None
"""
s = requests.Session(impersonate="chrome")
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
}
# 1. 访问登录页
r1 = s.get("https://app.tavily.com/sign-in", headers=headers)
if r1.status_code != 200:
print("❌ 无法访问登录页")
return None
# 提取 state
state_m = re.search(r'state=([^"&]+)', r1.text)
if not state_m:
print("❌ 未找到 state")
return None
state = state_m.group(1)
# 2. 第一次 Turnstile
print("🔐 Turnstile (1/2)...")
token1 = solve_turnstile(f"https://auth.tavily.com/u/login/identifier?state={state}")
if not token1:
return None
print("✅ Turnstile OK")
# 3. 提交邮箱
r2 = s.post(
f"https://auth.tavily.com/u/login/identifier?state={state}",
data={
"state": state,
"username": email,
"action": "default",
"captcha": token1
},
headers=headers,
allow_redirects=False
)
if r2.status_code != 302:
print("❌ 邮箱提交失败")
return None
# 检查是否跳转到注册页
loc = r2.headers.get('Location', '')
if '/login/password' in loc:
# 已有账号,切换到注册
r2_text = s.get(f"https://auth.tavily.com{loc}", headers=headers).text
signup_m = re.search(r'href="(/u/signup/identifier[^"]*)"', r2_text)
if not signup_m:
print("❌ 无法切换到注册页")
return None
signup_url = f"https://auth.tavily.com{signup_m.group(1)}"
r2 = s.get(signup_url, headers=headers)
# 重新提取 state
state_m2 = re.search(r'name="state"\s+value="([^"]+)"', r2.text)
if state_m2:
state = state_m2.group(1)
# 第二次 Turnstile
print("🔐 Turnstile (2/2)...")
token2 = solve_turnstile(signup_url)
if not token2:
return None
print("✅ Turnstile OK")
# 提交注册
r3 = s.post(
signup_url,
data={
"state": state,
"email": email,
"action": "default",
"captcha": token2
},
headers=headers,
allow_redirects=True
)
else:
r3 = s.get(f"https://auth.tavily.com{loc}", headers=headers, allow_redirects=True)
# 4. 到达密码页
if '/password' not in r3.url:
print(f"❌ 未到达密码页面: {r3.url}")
return None
# 第三次 Turnstile
print("🔐 Turnstile (3/3)...")
token3 = solve_turnstile(r3.url)
if not token3:
return None
print("✅ Turnstile OK")
# 提取 state 和 email
state_m3 = re.search(r'name="state"\s+value="([^"]+)"', r3.text)
if state_m3:
state = state_m3.group(1)
email_m = re.search(r'name="email"\s+value="([^"]*)"', r3.text)
email_val = email_m.group(1) if email_m else email
# 5. 提交密码
r4 = s.post(
r3.url,
data={
"state": state,
"email": email_val,
"password": password,
"action": "default",
"captcha": token3
},
headers=headers,
allow_redirects=False
)
if r4.status_code != 302:
print(f"❌ 密码提交失败 (状态码: {r4.status_code})")
return None
# 6. 跟随回调
callback = r4.headers.get('Location', '')
if callback.startswith('/'):
callback = f"https://auth.tavily.com{callback}"
r5 = s.get(callback, allow_redirects=True)
# 7. 检查是否需要邮件验证
if 'verify' in r5.url.lower() or 'verification' in r5.text.lower():
print("📧 需要邮件验证")
verify_url = get_verification_link(email)
if not verify_url:
print("❌ 未收到验证邮件")
_save_account(email, password, None)
return None
# 点击验证链接
print("🔗 点击验证链接...")
s.get(verify_url, allow_redirects=True)
time.sleep(2)
# 重新访问主页
r5 = s.get("https://app.tavily.com/home", allow_redirects=True)
if 'app.tavily.com' not in r5.url:
print("❌ 登录失败")
return None
# 8. 获取 API Key
api_key = get_api_key(s, email, password)
if api_key:
_save_account(email, password, api_key)
print(f"🎉 注册成功")
print(f" 邮箱: {email}")
print(f" 密码: {password}")
print(f" Key : {api_key}")
return api_key
else:
_save_account(email, password, None)
print("⚠️ 注册成功,未获取到 API Key")
print(f" 邮箱: {email}")
print(f" 密码: {password}")
return "SUCCESS_NO_KEY"
def get_api_key(session, email, password):
"""获取 API Key"""
time.sleep(3)
session.get("https://app.tavily.com/home")
time.sleep(2)
r_orgs = session.get("https://app.tavily.com/api/organizations")
if r_orgs.status_code != 200:
return None
orgs = r_orgs.json()
# 如果没有组织,创建一个
if not orgs:
r_create = session.post(
"https://app.tavily.com/api/organizations",
json={"name": "My Organization"},
headers={"Content-Type": "application/json"}
)
if r_create.status_code != 201:
return None
orgs = [r_create.json()]
org_id = orgs[0].get('id') or orgs[0].get('oid')
if not org_id:
return None
# 获取 API Keys
r_keys = session.get(f"https://app.tavily.com/api/keys?oid={org_id}")
if r_keys.status_code == 200:
keys = r_keys.json()
if keys and len(keys) > 0:
return keys[0].get('key')
# 尝试创建 API Key
r_create_key = session.post(
f"https://app.tavily.com/api/keys?oid={org_id}",
json={},
headers={"Content-Type": "application/json"}
)
if r_create_key.status_code in (200, 201):
key_data = r_create_key.json()
return key_data.get('key')
# 如果还是失败,尝试邮件验证
print("📧 尝试邮件验证...")
verify_url = get_verification_link(email)
if verify_url:
print("🔗 点击验证链接...")
session.get(verify_url, allow_redirects=True)
time.sleep(3)
# 重新尝试获取 API Key
r_keys2 = session.get(f"https://app.tavily.com/api/keys?oid={org_id}")
if r_keys2.status_code == 200:
keys2 = r_keys2.json()
if keys2 and len(keys2) > 0:
return keys2[0].get('key')
return None