feat: 增加key验证脚本
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
*.txt
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
9
Makefile
9
Makefile
@@ -180,6 +180,12 @@ compose-down:
|
|||||||
docker-compose down
|
docker-compose down
|
||||||
docker-compose -f docker-compose.dev.yml down 2>/dev/null || true
|
docker-compose -f docker-compose.dev.yml down 2>/dev/null || true
|
||||||
|
|
||||||
|
# 密钥验证
|
||||||
|
.PHONY: validate-keys
|
||||||
|
validate-keys:
|
||||||
|
@echo "🐍 使用 Python 版本验证密钥..."
|
||||||
|
python3 scripts/validate-keys.py -c 300 -t 15
|
||||||
|
|
||||||
# 健康检查
|
# 健康检查
|
||||||
.PHONY: health
|
.PHONY: health
|
||||||
health:
|
health:
|
||||||
@@ -245,3 +251,6 @@ help:
|
|||||||
@echo " stats - 查看统计信息"
|
@echo " stats - 查看统计信息"
|
||||||
@echo " reset-keys - 重置密钥状态"
|
@echo " reset-keys - 重置密钥状态"
|
||||||
@echo " blacklist - 查看黑名单"
|
@echo " blacklist - 查看黑名单"
|
||||||
|
@echo ""
|
||||||
|
@echo "密钥验证:"
|
||||||
|
@echo " validate-keys - 验证 API 密钥"
|
||||||
|
14
README.md
14
README.md
@@ -118,6 +118,20 @@ cp .env.example .env
|
|||||||
| CORS | `ENABLE_CORS` | true | 是否启用 CORS |
|
| CORS | `ENABLE_CORS` | true | 是否启用 CORS |
|
||||||
| 连接池 | `MAX_SOCKETS` | 50 | HTTP 连接池最大连接数 |
|
| 连接池 | `MAX_SOCKETS` | 50 | HTTP 连接池最大连接数 |
|
||||||
|
|
||||||
|
## 🔑 密钥验证工具
|
||||||
|
|
||||||
|
项目提供了高性能的 API 密钥验证工具,支持批量验证、去重和多模型测试:
|
||||||
|
|
||||||
|
### 快速使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 自动选择最佳验证方式
|
||||||
|
make validate-keys
|
||||||
|
|
||||||
|
# 或直接运行
|
||||||
|
./scripts/validate-keys.py
|
||||||
|
```
|
||||||
|
|
||||||
## 📊 监控端点
|
## 📊 监控端点
|
||||||
|
|
||||||
| 端点 | 方法 | 说明 |
|
| 端点 | 方法 | 说明 |
|
||||||
|
335
scripts/validate-keys.py
Executable file
335
scripts/validate-keys.py
Executable file
@@ -0,0 +1,335 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
高性能 OpenAI API 密钥验证脚本
|
||||||
|
支持并发验证、去重、多模型测试
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from typing import List, Dict, Set, Tuple
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
DEFAULT_KEYS_FILE = "keys.txt"
|
||||||
|
DEFAULT_BASE_URL = "https://api.openai.com"
|
||||||
|
DEFAULT_CONCURRENCY = 50
|
||||||
|
DEFAULT_TIMEOUT = 30
|
||||||
|
|
||||||
|
# 测试模型列表
|
||||||
|
TEST_MODELS = [
|
||||||
|
"gpt-4o-mini",
|
||||||
|
"gpt-4.1-mini",
|
||||||
|
"gpt-4.1-nano"
|
||||||
|
]
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class KeyValidationResult:
|
||||||
|
"""密钥验证结果"""
|
||||||
|
key: str
|
||||||
|
key_preview: str
|
||||||
|
is_valid: bool
|
||||||
|
model_results: Dict[str, bool]
|
||||||
|
error_message: str = ""
|
||||||
|
|
||||||
|
class KeyValidator:
|
||||||
|
"""高性能密钥验证器"""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str = DEFAULT_BASE_URL, timeout: int = DEFAULT_TIMEOUT, concurrency: int = DEFAULT_CONCURRENCY):
|
||||||
|
self.base_url = base_url.rstrip('/')
|
||||||
|
self.timeout = timeout
|
||||||
|
self.concurrency = concurrency
|
||||||
|
self.session = None
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
"""异步上下文管理器入口"""
|
||||||
|
connector = aiohttp.TCPConnector(
|
||||||
|
limit=self.concurrency * 2,
|
||||||
|
limit_per_host=self.concurrency,
|
||||||
|
ttl_dns_cache=300,
|
||||||
|
use_dns_cache=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
timeout = aiohttp.ClientTimeout(total=self.timeout)
|
||||||
|
self.session = aiohttp.ClientSession(
|
||||||
|
connector=connector,
|
||||||
|
timeout=timeout,
|
||||||
|
headers={
|
||||||
|
'User-Agent': 'GPT-Load-KeyValidator/1.0'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""异步上下文管理器退出"""
|
||||||
|
if self.session:
|
||||||
|
await self.session.close()
|
||||||
|
|
||||||
|
def get_key_preview(self, key: str) -> str:
|
||||||
|
"""获取密钥预览(脱敏显示)"""
|
||||||
|
if len(key) < 20:
|
||||||
|
return key[:4] + "***" + key[-4:]
|
||||||
|
return key[:8] + "***" + key[-8:]
|
||||||
|
|
||||||
|
async def test_model(self, key: str, model: str) -> bool:
|
||||||
|
"""测试单个模型是否可用"""
|
||||||
|
url = f"{self.base_url}/v1/chat/completions"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": "Hi"}
|
||||||
|
],
|
||||||
|
"max_tokens": 1,
|
||||||
|
"temperature": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with self.session.post(url, headers=headers, json=payload) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
return True
|
||||||
|
elif response.status == 401:
|
||||||
|
# 认证失败,密钥无效
|
||||||
|
return False
|
||||||
|
elif response.status == 404:
|
||||||
|
# 模型不存在或无权限
|
||||||
|
return False
|
||||||
|
elif response.status == 429:
|
||||||
|
# 速率限制,但密钥可能有效
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# 其他错误,认为模型不可用
|
||||||
|
return False
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def validate_key(self, key: str) -> KeyValidationResult:
|
||||||
|
"""验证单个密钥"""
|
||||||
|
key_preview = self.get_key_preview(key)
|
||||||
|
model_results = {}
|
||||||
|
|
||||||
|
# 并发测试所有模型
|
||||||
|
tasks = []
|
||||||
|
for model in TEST_MODELS:
|
||||||
|
task = asyncio.create_task(self.test_model(key, model))
|
||||||
|
tasks.append((model, task))
|
||||||
|
|
||||||
|
# 等待所有测试完成
|
||||||
|
for model, task in tasks:
|
||||||
|
try:
|
||||||
|
result = await task
|
||||||
|
model_results[model] = result
|
||||||
|
except Exception as e:
|
||||||
|
model_results[model] = False
|
||||||
|
|
||||||
|
# 判断密钥是否有效(至少一个模型可用)
|
||||||
|
is_valid = any(model_results.values())
|
||||||
|
|
||||||
|
return KeyValidationResult(
|
||||||
|
key=key,
|
||||||
|
key_preview=key_preview,
|
||||||
|
is_valid=is_valid,
|
||||||
|
model_results=model_results
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_keys(file_path: str) -> List[str]:
|
||||||
|
"""加载密钥文件"""
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
print(f"❌ 密钥文件不存在: {file_path}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
keys = []
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
for line_num, line in enumerate(f, 1):
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith('#'):
|
||||||
|
keys.append(line)
|
||||||
|
|
||||||
|
return keys
|
||||||
|
|
||||||
|
def deduplicate_keys(keys: List[str]) -> List[str]:
|
||||||
|
"""去重密钥"""
|
||||||
|
seen = set()
|
||||||
|
unique_keys = []
|
||||||
|
|
||||||
|
for key in keys:
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
unique_keys.append(key)
|
||||||
|
|
||||||
|
return unique_keys
|
||||||
|
|
||||||
|
def format_model_status(model_results: Dict[str, bool]) -> str:
|
||||||
|
"""格式化模型状态显示"""
|
||||||
|
status_parts = []
|
||||||
|
for model in TEST_MODELS:
|
||||||
|
if model in model_results:
|
||||||
|
emoji = "✅" if model_results[model] else "❌"
|
||||||
|
status_parts.append(f"{emoji} {model}")
|
||||||
|
else:
|
||||||
|
status_parts.append(f"❓ {model}")
|
||||||
|
|
||||||
|
return " | ".join(status_parts)
|
||||||
|
|
||||||
|
async def validate_keys_batch(keys: List[str], base_url: str, timeout: int, concurrency: int) -> List[KeyValidationResult]:
|
||||||
|
"""批量验证密钥"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
async with KeyValidator(base_url, timeout, concurrency) as validator:
|
||||||
|
# 创建信号量限制并发数
|
||||||
|
semaphore = asyncio.Semaphore(concurrency)
|
||||||
|
|
||||||
|
async def validate_with_semaphore(key: str) -> KeyValidationResult:
|
||||||
|
async with semaphore:
|
||||||
|
return await validator.validate_key(key)
|
||||||
|
|
||||||
|
# 创建所有验证任务
|
||||||
|
tasks = [validate_with_semaphore(key) for key in keys]
|
||||||
|
|
||||||
|
# 使用 as_completed 来实时显示进度
|
||||||
|
completed = 0
|
||||||
|
total = len(tasks)
|
||||||
|
|
||||||
|
print(f"\n🚀 开始验证 {total} 个密钥...")
|
||||||
|
print("=" * 120)
|
||||||
|
print(f"{'序号':<6} {'密钥预览':<20} {'状态':<6} {'模型测试结果':<80}")
|
||||||
|
print("=" * 120)
|
||||||
|
|
||||||
|
for coro in asyncio.as_completed(tasks):
|
||||||
|
result = await coro
|
||||||
|
completed += 1
|
||||||
|
|
||||||
|
# 实时输出结果
|
||||||
|
status_emoji = "✅ 有效" if result.is_valid else "❌ 无效"
|
||||||
|
model_status = format_model_status(result.model_results)
|
||||||
|
|
||||||
|
print(f"{completed:<6} {result.key_preview:<20} {status_emoji:<6} {model_status}")
|
||||||
|
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def save_results(results: List[KeyValidationResult], output_dir: str = "."):
|
||||||
|
"""保存验证结果到文件"""
|
||||||
|
valid_keys = []
|
||||||
|
invalid_keys = []
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
if result.is_valid:
|
||||||
|
valid_keys.append(result.key)
|
||||||
|
else:
|
||||||
|
invalid_keys.append(result.key)
|
||||||
|
|
||||||
|
# 保存有效密钥
|
||||||
|
valid_file = os.path.join(output_dir, "valid_keys.txt")
|
||||||
|
with open(valid_file, 'w', encoding='utf-8') as f:
|
||||||
|
for key in valid_keys:
|
||||||
|
f.write(f"{key}\n")
|
||||||
|
|
||||||
|
# 保存无效密钥
|
||||||
|
invalid_file = os.path.join(output_dir, "invalid_keys.txt")
|
||||||
|
with open(invalid_file, 'w', encoding='utf-8') as f:
|
||||||
|
for key in invalid_keys:
|
||||||
|
f.write(f"{key}\n")
|
||||||
|
|
||||||
|
return valid_file, invalid_file, len(valid_keys), len(invalid_keys)
|
||||||
|
|
||||||
|
def print_summary(results: List[KeyValidationResult], valid_count: int, invalid_count: int,
|
||||||
|
valid_file: str, invalid_file: str, duration: float):
|
||||||
|
"""打印验证总结"""
|
||||||
|
total = len(results)
|
||||||
|
|
||||||
|
print("\n" + "=" * 120)
|
||||||
|
print("📊 验证结果总结")
|
||||||
|
print("=" * 120)
|
||||||
|
print(f"总密钥数量: {total}")
|
||||||
|
print(f"有效密钥数: {valid_count} ({valid_count/total*100:.1f}%)")
|
||||||
|
print(f"无效密钥数: {invalid_count} ({invalid_count/total*100:.1f}%)")
|
||||||
|
print(f"验证耗时: {duration:.2f} 秒")
|
||||||
|
print(f"平均速度: {total/duration:.1f} 密钥/秒")
|
||||||
|
print()
|
||||||
|
print(f"📁 结果文件:")
|
||||||
|
print(f" 有效密钥: {valid_file}")
|
||||||
|
print(f" 无效密钥: {invalid_file}")
|
||||||
|
|
||||||
|
# 模型统计
|
||||||
|
print(f"\n📈 模型可用性统计:")
|
||||||
|
model_stats = {model: 0 for model in TEST_MODELS}
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
if result.is_valid:
|
||||||
|
for model, available in result.model_results.items():
|
||||||
|
if available:
|
||||||
|
model_stats[model] += 1
|
||||||
|
|
||||||
|
for model in TEST_MODELS:
|
||||||
|
count = model_stats[model]
|
||||||
|
percentage = count / valid_count * 100 if valid_count > 0 else 0
|
||||||
|
print(f" {model}: {count}/{valid_count} ({percentage:.1f}%)")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""主函数"""
|
||||||
|
parser = argparse.ArgumentParser(description="OpenAI API 密钥验证工具")
|
||||||
|
parser.add_argument("-f", "--file", default=DEFAULT_KEYS_FILE, help="密钥文件路径")
|
||||||
|
parser.add_argument("-u", "--url", default=DEFAULT_BASE_URL, help="API 基础URL")
|
||||||
|
parser.add_argument("-c", "--concurrency", type=int, default=DEFAULT_CONCURRENCY, help="并发数")
|
||||||
|
parser.add_argument("-t", "--timeout", type=int, default=DEFAULT_TIMEOUT, help="超时时间(秒)")
|
||||||
|
parser.add_argument("-o", "--output", default=".", help="输出目录")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print("🔑 OpenAI API 密钥验证工具")
|
||||||
|
print(f"📁 密钥文件: {args.file}")
|
||||||
|
print(f"🌐 API地址: {args.url}")
|
||||||
|
print(f"⚡ 并发数: {args.concurrency}")
|
||||||
|
print(f"⏱️ 超时时间: {args.timeout}秒")
|
||||||
|
print(f"🧪 测试模型: {', '.join(TEST_MODELS)}")
|
||||||
|
|
||||||
|
# 加载和去重密钥
|
||||||
|
print(f"\n📖 加载密钥文件...")
|
||||||
|
raw_keys = load_keys(args.file)
|
||||||
|
print(f" 原始密钥数量: {len(raw_keys)}")
|
||||||
|
|
||||||
|
unique_keys = deduplicate_keys(raw_keys)
|
||||||
|
duplicates = len(raw_keys) - len(unique_keys)
|
||||||
|
print(f" 去重后数量: {len(unique_keys)}")
|
||||||
|
if duplicates > 0:
|
||||||
|
print(f" 发现重复: {duplicates} 个")
|
||||||
|
|
||||||
|
if not unique_keys:
|
||||||
|
print("❌ 没有找到有效的密钥")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 开始验证
|
||||||
|
start_time = time.time()
|
||||||
|
results = await validate_keys_batch(unique_keys, args.url, args.timeout, args.concurrency)
|
||||||
|
duration = time.time() - start_time
|
||||||
|
|
||||||
|
# 保存结果
|
||||||
|
valid_file, invalid_file, valid_count, invalid_count = save_results(results, args.output)
|
||||||
|
|
||||||
|
# 打印总结
|
||||||
|
print_summary(results, valid_count, invalid_count, valid_file, invalid_file, duration)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\n⚠️ 用户中断验证过程")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ 验证过程中发生错误: {e}")
|
||||||
|
sys.exit(1)
|
Reference in New Issue
Block a user