feat: 添加Web验证页面并优化密钥管理功能

This commit is contained in:
yinpeng
2025-02-11 20:45:49 +08:00
parent d1f8a98ad0
commit 8c9b802016
9 changed files with 526 additions and 49 deletions
+69 -11
View File
@@ -4,11 +4,11 @@
## 📝 项目简介 ## 📝 项目简介
本项目是一个基于 FastAPI 框架开发的高性能、易于部署的 OpenAI 和 Gemini API 代理服务。它不仅兼容 OpenAI 的 API 接口,还支持 Google 的 Gemini 模型,为用户提供灵活的模型选择。该代理服务内置了多 API Key 轮询、负载均衡、自动重试、访问控制(Bearer Token 认证)、流式响应等功能,旨在简化 AI 应用的开发和部署流程。 本项目是一个基于 FastAPI 框架开发的高性能、易于部署的Gemini OpenAI兼容 和 Gemini API 代理服务。它不仅兼容 OpenAI 的 API 接口,还支持 Google 的 Gemini 原生接口。该代理服务内置了多 API Key 轮询、负载均衡、自动重试、访问控制(Bearer Token 认证)、流式响应等功能,旨在简化 AI 应用的开发和部署流程。
**核心功能与优势:** **核心功能与优势:**
- **多模型支持**: 无缝切换 OpenAI 和 Gemini 模型 - **多协议支持**: 无缝切换 OpenAI兼容 和 Gemini 协议
- **智能 API Key 管理**: 自动轮询多个 API Key,实现负载均衡和故障转移。 - **智能 API Key 管理**: 自动轮询多个 API Key,实现负载均衡和故障转移。
- **安全访问控制**: 使用 Bearer Token 进行身份验证,保护 API 访问。 - **安全访问控制**: 使用 Bearer Token 进行身份验证,保护 API 访问。
- **流式响应支持**: 提供实时的流式数据传输,提升用户体验。 - **流式响应支持**: 提供实时的流式数据传输,提升用户体验。
@@ -170,13 +170,30 @@ uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
所有 API 请求都需要在 Header 中添加 `Authorization` 字段,值为 `Bearer <your-token>`,其中 `<your-token>` 需要替换为你在 `.env` 文件中配置的 `ALLOWED_TOKENS` 中的一个或者 `AUTH_TOKEN`。 所有 API 请求都需要在 Header 中添加 `Authorization` 字段,值为 `Bearer <your-token>`,其中 `<your-token>` 需要替换为你在 `.env` 文件中配置的 `ALLOWED_TOKENS` 中的一个或者 `AUTH_TOKEN`。
### 获取模型列表 ### API 路由
本服务提供两种API路由:
1. **OpenAI 兼容路由** (推荐)
- 基础路径: `/v1`
- 完全兼容OpenAI API格式
- 支持所有Gemini模型
2. **Gemini 原生路由**
- 基础路径: `/gemini/v1beta` 或 `/v1beta`
- 遵循Google原生API格式
- 适用于需要直接使用Gemini API的场景
### OpenAI兼容路由
#### 获取模型列表
- **URL**: `/v1/models` - **URL**: `/v1/models`
- **Method**: `GET` - **Method**: `GET`
- **Header**: `Authorization: Bearer <your-token>` - **Header**: `Authorization: Bearer <your-token>`
- **Response**: 返回支持的所有模型列表,包括最新的`gemini-2.0-flash-exp-search`等模型
### 聊天补全 (Chat Completions) #### 聊天补全 (Chat Completions)
- **URL**: `/v1/chat/completions` - **URL**: `/v1/chat/completions`
- **Method**: `POST` - **Method**: `POST`
@@ -202,11 +219,34 @@ uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
} }
``` ```
- `messages`: 消息列表,格式与 OpenAI API 相同 - `messages`: 消息列表,格式与 OpenAI API 相同
- `model`: 模型名称,例如 `gemini-1.5-flash-002`。 - `model`: 模型名称,支持所有Gemini模型,包括:
- `stream`: 是否开启流式响应,`true` 或 `false`。 - `gemini-1.5-flash-002`: 快速响应模型
- `tools`: 使用的工具列表。 - `gemini-2.0-flash-exp`: 实验性快速响应模型
- 其他参数:与 OpenAI API 兼容的参数,如 `temperature`, `max_tokens` 等。 - `gemini-2.0-flash-exp-search`: 支持搜索功能的实验性模型
- `stream`: 是否开启流式响应,`true` 或 `false`
- `tools`: 使用的工具列表
- 其他参数:与 OpenAI API 兼容的参数,如 `temperature`, `max_tokens` 等
### Gemini原生路由
#### 获取模型列表
- **URL**: `/gemini/v1beta/models` 或 `/v1beta/models`
- **Method**: `GET`
- **Header**: `Authorization: Bearer <your-token>`
#### 生成内容
- **URL**: `/gemini/v1beta/models/{model_name}:generateContent`
- **Method**: `POST`
- **Header**: `Authorization: Bearer <your-token>`
#### 流式生成内容
- **URL**: `/gemini/v1beta/models/{model_name}:streamGenerateContent`
- **Method**: `POST`
- **Header**: `Authorization: Bearer <your-token>`
### 获取词向量 (Embeddings) ### 获取词向量 (Embeddings)
@@ -230,12 +270,30 @@ uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
- **URL**: `/health` - **URL**: `/health`
- **Method**: `GET` - **Method**: `GET`
### 获取 API Key 列表 ### Web界面功能
#### 验证页面
- **URL**: `/auth`
- **说明**: 提供了一个简洁的Web界面用于验证访问令牌
- **功能**:
- 美观的用户界面,支持响应式设计
- 安全的令牌验证机制
- 错误提示功能
- 支持移动端访问
#### API密钥状态管理
- **URL**: `/v1/keys/list` - **URL**: `/v1/keys/list`
- **Method**: `GET` - **Method**: `GET`
- **Header**: `Authorization: Bearer <your-auth-token>` - **Header**: `Authorization: Bearer <your-auth-token>`
- **说明**: 只有使用 `AUTH_TOKEN` 才能访问此接口, 用于获取有效和无效的 API Key 列表。 - **说明**:
- 只有使用 `AUTH_TOKEN` 才能访问此接口
- 提供了可视化的Web界面展示API密钥状态
- 支持查看有效和无效的API密钥列表
- 显示每个密钥的失败次数统计
- 提供一键复制功能(支持复制单个密钥或批量复制)
- 实时显示密钥总数统计
### 图片生成 (Image Generation) ### 图片生成 (Image Generation)
+16 -7
View File
@@ -6,7 +6,7 @@ from app.core.logger import get_gemini_logger
from app.core.security import SecurityService from app.core.security import SecurityService
from app.schemas.gemini_models import GeminiRequest from app.schemas.gemini_models import GeminiRequest
from app.services.gemini_chat_service import GeminiChatService from app.services.gemini_chat_service import GeminiChatService
from app.services.key_manager import KeyManager from app.services.key_manager import KeyManager, get_key_manager_instance
from app.services.model_service import ModelService from app.services.model_service import ModelService
from app.services.chat.retry_handler import RetryHandler from app.services.chat.retry_handler import RetryHandler
@@ -16,13 +16,20 @@ logger = get_gemini_logger()
# 初始化服务 # 初始化服务
security_service = SecurityService(settings.ALLOWED_TOKENS, settings.AUTH_TOKEN) security_service = SecurityService(settings.ALLOWED_TOKENS, settings.AUTH_TOKEN)
key_manager = KeyManager(settings.API_KEYS)
async def get_key_manager():
return await get_key_manager_instance()
async def get_next_working_key_wrapper(key_manager: KeyManager = Depends(get_key_manager)):
return await key_manager.get_next_working_key()
model_service = ModelService(settings.MODEL_SEARCH) model_service = ModelService(settings.MODEL_SEARCH)
@router.get("/models") @router.get("/models")
@router_v1beta.get("/models") @router_v1beta.get("/models")
async def list_models(_=Depends(security_service.verify_key)): async def list_models(_=Depends(security_service.verify_key),
key_manager: KeyManager = Depends(get_key_manager)):
"""获取可用的Gemini模型列表""" """获取可用的Gemini模型列表"""
logger.info("-" * 50 + "list_gemini_models" + "-" * 50) logger.info("-" * 50 + "list_gemini_models" + "-" * 50)
logger.info("Handling Gemini models list request") logger.info("Handling Gemini models list request")
@@ -40,12 +47,13 @@ async def list_models(_=Depends(security_service.verify_key)):
@router.post("/models/{model_name}:generateContent") @router.post("/models/{model_name}:generateContent")
@router_v1beta.post("/models/{model_name}:generateContent") @router_v1beta.post("/models/{model_name}:generateContent")
@RetryHandler(max_retries=3, key_manager=key_manager, key_arg="api_key") @RetryHandler(max_retries=3, key_manager=Depends(get_key_manager), key_arg="api_key")
async def generate_content( async def generate_content(
model_name: str, model_name: str,
request: GeminiRequest, request: GeminiRequest,
_=Depends(security_service.verify_goog_api_key), _=Depends(security_service.verify_goog_api_key),
api_key: str = Depends(key_manager.get_next_working_key), api_key: str = Depends(get_next_working_key_wrapper),
key_manager: KeyManager = Depends(get_key_manager)
): ):
chat_service = GeminiChatService(settings.BASE_URL, key_manager) chat_service = GeminiChatService(settings.BASE_URL, key_manager)
"""非流式生成内容""" """非流式生成内容"""
@@ -69,12 +77,13 @@ async def generate_content(
@router.post("/models/{model_name}:streamGenerateContent") @router.post("/models/{model_name}:streamGenerateContent")
@router_v1beta.post("/models/{model_name}:streamGenerateContent") @router_v1beta.post("/models/{model_name}:streamGenerateContent")
@RetryHandler(max_retries=3, key_manager=key_manager, key_arg="api_key") @RetryHandler(max_retries=3, key_manager=Depends(get_key_manager), key_arg="api_key")
async def stream_generate_content( async def stream_generate_content(
model_name: str, model_name: str,
request: GeminiRequest, request: GeminiRequest,
_=Depends(security_service.verify_goog_api_key), _=Depends(security_service.verify_goog_api_key),
api_key: str = Depends(key_manager.get_next_working_key), api_key: str = Depends(get_next_working_key_wrapper),
key_manager: KeyManager = Depends(get_key_manager)
): ):
chat_service = GeminiChatService(settings.BASE_URL, key_manager) chat_service = GeminiChatService(settings.BASE_URL, key_manager)
"""流式生成内容""" """流式生成内容"""
+23 -18
View File
@@ -8,7 +8,7 @@ from app.schemas.openai_models import ChatRequest, EmbeddingRequest, ImageGenera
from app.services.chat.retry_handler import RetryHandler from app.services.chat.retry_handler import RetryHandler
from app.services.embedding_service import EmbeddingService from app.services.embedding_service import EmbeddingService
from app.services.image_create_service import ImageCreateService from app.services.image_create_service import ImageCreateService
from app.services.key_manager import KeyManager from app.services.key_manager import KeyManager, get_key_manager_instance
from app.services.model_service import ModelService from app.services.model_service import ModelService
from app.services.openai_chat_service import OpenAIChatService from app.services.openai_chat_service import OpenAIChatService
@@ -17,15 +17,22 @@ logger = get_openai_logger()
# 初始化服务 # 初始化服务
security_service = SecurityService(settings.ALLOWED_TOKENS, settings.AUTH_TOKEN) security_service = SecurityService(settings.ALLOWED_TOKENS, settings.AUTH_TOKEN)
key_manager = KeyManager(settings.API_KEYS)
model_service = ModelService(settings.MODEL_SEARCH) model_service = ModelService(settings.MODEL_SEARCH)
embedding_service = EmbeddingService(settings.BASE_URL) embedding_service = EmbeddingService(settings.BASE_URL)
image_create_service = ImageCreateService() image_create_service = ImageCreateService()
async def get_key_manager():
return await get_key_manager_instance()
async def get_next_working_key_wrapper(key_manager: KeyManager = Depends(get_key_manager)):
return await key_manager.get_next_working_key()
@router.get("/v1/models") @router.get("/v1/models")
@router.get("/hf/v1/models") @router.get("/hf/v1/models")
async def list_models(_=Depends(security_service.verify_authorization)): async def list_models(
_=Depends(security_service.verify_authorization),
key_manager: KeyManager = Depends(get_key_manager)
):
logger.info("-" * 50 + "list_models" + "-" * 50) logger.info("-" * 50 + "list_models" + "-" * 50)
logger.info("Handling models list request") logger.info("Handling models list request")
api_key = await key_manager.get_next_working_key() api_key = await key_manager.get_next_working_key()
@@ -39,11 +46,12 @@ async def list_models(_=Depends(security_service.verify_authorization)):
@router.post("/v1/chat/completions") @router.post("/v1/chat/completions")
@router.post("/hf/v1/chat/completions") @router.post("/hf/v1/chat/completions")
@RetryHandler(max_retries=3, key_manager=key_manager, key_arg="api_key") @RetryHandler(max_retries=3, key_manager=Depends(get_key_manager), key_arg="api_key")
async def chat_completion( async def chat_completion(
request: ChatRequest, request: ChatRequest,
_=Depends(security_service.verify_authorization), _=Depends(security_service.verify_authorization),
api_key: str = Depends(key_manager.get_next_working_key), api_key: str = Depends(get_next_working_key_wrapper),
key_manager: KeyManager = Depends(get_key_manager)
): ):
# 如果model是imagen3,使用paid_key # 如果model是imagen3,使用paid_key
if request.model == f"{settings.CREATE_IMAGE_MODEL}-chat": if request.model == f"{settings.CREATE_IMAGE_MODEL}-chat":
@@ -58,23 +66,21 @@ async def chat_completion(
if request.model == f"{settings.CREATE_IMAGE_MODEL}-chat": if request.model == f"{settings.CREATE_IMAGE_MODEL}-chat":
response = await chat_service.create_image_chat_completion(request=request) response = await chat_service.create_image_chat_completion(request=request)
else: else:
response = await chat_service.create_chat_completion(request,api_key) response = await chat_service.create_chat_completion(request, api_key)
# 处理流式响应 # 处理流式响应
if request.stream: if request.stream:
return StreamingResponse(response, media_type="text/event-stream") return StreamingResponse(response, media_type="text/event-stream")
logger.info("Chat completion request successful") logger.info("Chat completion request successful")
return response return response
except Exception as e: except Exception as e:
logger.error(f"Chat completion failed after retries: {str(e)}") logger.error(f"Chat completion failed after retries: {str(e)}")
raise HTTPException(status_code=500, detail="Chat completion failed") from e raise HTTPException(status_code=500, detail="Chat completion failed") from e
@router.post("/v1/images/generations") @router.post("/v1/images/generations")
@router.post("/hf/v1/images/generations") @router.post("/hf/v1/images/generations")
async def generate_image( async def generate_image(
request: ImageGenerationRequest, request: ImageGenerationRequest,
_=Depends(security_service.verify_authorization), _=Depends(security_service.verify_authorization),
): ):
logger.info("-" * 50 + "generate_image" + "-" * 50) logger.info("-" * 50 + "generate_image" + "-" * 50)
logger.info(f"Handling image generation request for prompt: {request.prompt}") logger.info(f"Handling image generation request for prompt: {request.prompt}")
@@ -83,17 +89,16 @@ async def generate_image(
response = image_create_service.generate_images(request) response = image_create_service.generate_images(request)
logger.info("Image generation request successful") logger.info("Image generation request successful")
return response return response
except Exception as e: except Exception as e:
logger.error(f"Image generation request failed: {str(e)}") logger.error(f"Image generation request failed: {str(e)}")
raise HTTPException(status_code=500, detail="Image generation request failed") from e raise HTTPException(status_code=500, detail="Image generation request failed") from e
@router.post("/v1/embeddings") @router.post("/v1/embeddings")
@router.post("/hf/v1/embeddings") @router.post("/hf/v1/embeddings")
async def embedding( async def embedding(
request: EmbeddingRequest, request: EmbeddingRequest,
_=Depends(security_service.verify_authorization), _=Depends(security_service.verify_authorization),
key_manager: KeyManager = Depends(get_key_manager)
): ):
logger.info("-" * 50 + "embedding" + "-" * 50) logger.info("-" * 50 + "embedding" + "-" * 50)
logger.info(f"Handling embedding request for model: {request.model}") logger.info(f"Handling embedding request for model: {request.model}")
@@ -109,11 +114,11 @@ async def embedding(
logger.error(f"Embedding request failed: {str(e)}") logger.error(f"Embedding request failed: {str(e)}")
raise HTTPException(status_code=500, detail="Embedding request failed") from e raise HTTPException(status_code=500, detail="Embedding request failed") from e
@router.get("/v1/keys/list") @router.get("/v1/keys/list")
@router.get("/hf/v1/keys/list") @router.get("/hf/v1/keys/list")
async def get_keys_list( async def get_keys_list(
_=Depends(security_service.verify_auth_token), _=Depends(security_service.verify_auth_token),
key_manager: KeyManager = Depends(get_key_manager)
): ):
"""获取有效和无效的API key列表""" """获取有效和无效的API key列表"""
logger.info("-" * 50 + "get_keys_list" + "-" * 50) logger.info("-" * 50 + "get_keys_list" + "-" * 50)
+3
View File
@@ -1,9 +1,12 @@
from fastapi import HTTPException, Header from fastapi import HTTPException, Header
from typing import Optional from typing import Optional
from app.core.logger import get_security_logger from app.core.logger import get_security_logger
from app.core.config import settings
logger = get_security_logger() logger = get_security_logger()
def verify_auth_token(token: str) -> bool:
return token == settings.AUTH_TOKEN
class SecurityService: class SecurityService:
def __init__(self, allowed_tokens: list, auth_token: str): def __init__(self, allowed_tokens: list, auth_token: str):
+77 -4
View File
@@ -1,17 +1,53 @@
from fastapi import FastAPI from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from app.core.logger import get_main_logger from app.core.logger import get_main_logger
from app.core.security import verify_auth_token
from app.services.key_manager import get_key_manager_instance
from app.core.config import settings
from app.api import gemini_routes, openai_routes from app.api import gemini_routes, openai_routes
import uvicorn import uvicorn
from app.middleware.request_logging_middleware import RequestLoggingMiddleware
# 配置日志 # 配置日志
logger = get_main_logger() logger = get_main_logger()
app = FastAPI() app = FastAPI()
# 配置Jinja2模板
templates = Jinja2Templates(directory="app/templates")
# 创建 KeyManager 实例
key_manager = None
@app.on_event("startup")
async def startup_event():
global key_manager
key_manager = await get_key_manager_instance(settings.API_KEYS)
# 配置静态文件
app.mount("/static", StaticFiles(directory="app/static"), name="static")
# 添加中间件来处理未经身份验证的请求
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
# 允许 gemini_routes 和 openai_routes 中的端点绕过身份验证
if (request.url.path not in ["/", "/auth"] and
not request.url.path.startswith("/static") and
not request.url.path.startswith("/gemini") and
not request.url.path.startswith("/v1") and
not request.url.path.startswith("/v1beta") and
not request.url.path.startswith("/health") and
not request.url.path.startswith("/hf")):
auth_token = request.cookies.get("auth_token")
if not auth_token or not verify_auth_token(auth_token):
return RedirectResponse(url="/")
response = await call_next(request)
return response
# 添加请求日志中间件 # 添加请求日志中间件
# app.add_middleware(RequestLoggingMiddleware) # app.add_middleware(RequestLoggingMiddleware)
@@ -32,9 +68,46 @@ app.include_router(gemini_routes.router)
app.include_router(gemini_routes.router_v1beta) app.include_router(gemini_routes.router_v1beta)
@app.get("/", response_class=HTMLResponse)
async def auth_page(request: Request):
return templates.TemplateResponse("auth.html", {"request": request})
@app.post("/auth")
async def authenticate(request: Request):
try:
form = await request.form()
auth_token = form.get("auth_token")
if not auth_token:
return RedirectResponse(url="/", status_code=302)
if verify_auth_token(auth_token):
response = RedirectResponse(url="/keys", status_code=302)
response.set_cookie(key="auth_token", value=auth_token, httponly=True, max_age=3600)
return response
return RedirectResponse(url="/", status_code=302)
except Exception as e:
logger.error(f"Authentication error: {str(e)}")
return RedirectResponse(url="/", status_code=302)
@app.get("/keys", response_class=HTMLResponse)
async def keys_page(request: Request):
auth_token = request.cookies.get("auth_token")
if not auth_token or not verify_auth_token(auth_token):
return RedirectResponse(url="/", status_code=302)
keys_status = await key_manager.get_keys_by_status()
total = len(keys_status["valid_keys"]) + len(keys_status["invalid_keys"])
return templates.TemplateResponse("keys_status.html", {
"request": request,
"valid_keys": keys_status["valid_keys"],
"invalid_keys": keys_status["invalid_keys"],
"total": total
})
@app.get("/health") @app.get("/health")
@app.get("/") async def health_check(request: Request):
async def health_check():
logger.info("Health check endpoint called") logger.info("Health check endpoint called")
return {"status": "healthy"} return {"status": "healthy"}
+32 -7
View File
@@ -4,6 +4,7 @@ from typing import Dict
from app.core.logger import get_key_manager_logger from app.core.logger import get_key_manager_logger
from app.core.config import settings from app.core.config import settings
logger = get_key_manager_logger() logger = get_key_manager_logger()
@@ -61,20 +62,44 @@ class KeyManager:
return await self.get_next_working_key() return await self.get_next_working_key()
def get_fail_count(self, key: str) -> int:
"""获取指定密钥的失败次数"""
return self.key_failure_counts.get(key, 0)
async def get_keys_by_status(self) -> dict: async def get_keys_by_status(self) -> dict:
"""获取分类后的API key列表""" """获取分类后的API key列表,包括失败次数"""
valid_keys = [] valid_keys = {}
invalid_keys = [] invalid_keys = {}
async with self.failure_count_lock: async with self.failure_count_lock:
for key in self.api_keys: for key in self.api_keys:
masked_key = f"{key}" fail_count = self.key_failure_counts[key]
if self.key_failure_counts[key] < self.MAX_FAILURES: if fail_count < self.MAX_FAILURES:
valid_keys.append(masked_key) valid_keys[key] = fail_count
else: else:
invalid_keys.append(masked_key) invalid_keys[key] = fail_count
return { return {
"valid_keys": valid_keys, "valid_keys": valid_keys,
"invalid_keys": invalid_keys "invalid_keys": invalid_keys
} }
_singleton_instance = None
_singleton_lock = asyncio.Lock()
async def get_key_manager_instance(api_keys: list = None) -> KeyManager:
"""
获取 KeyManager 单例实例。
如果尚未创建实例,将使用提供的 api_keys 初始化 KeyManager。
如果已创建实例,则忽略 api_keys 参数,返回现有单例。
"""
global _singleton_instance
async with _singleton_lock:
if _singleton_instance is None:
if api_keys is None:
raise ValueError("API keys are required to initialize the KeyManager")
_singleton_instance = KeyManager(api_keys)
return _singleton_instance
+89
View File
@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>验证页面</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Roboto', sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
max-width: 400px;
width: 90%;
background: white;
padding: 40px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.container:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0,0,0,0.15);
}
h2 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
font-weight: 700;
}
form {
display: flex;
flex-direction: column;
}
input {
margin: 10px 0;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 5px;
font-size: 16px;
transition: all 0.3s ease;
}
input:focus {
border-color: #3498db;
box-shadow: 0 0 5px rgba(52, 152, 219, 0.5);
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 12px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #2980b9;
}
.error-message {
color: #e74c3c;
margin-top: 15px;
text-align: center;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h2>请输入验证令牌</h2>
<form id="auth-form" action="/auth" method="post">
<input type="password" id="auth-token" name="auth_token" required placeholder="输入验证令牌">
<button type="submit">提交</button>
</form>
{% if error %}
<p class="error-message">{{ error }}</p>
{% endif %}
</div>
</body>
</html>
+214
View File
@@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API密钥状态</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Roboto', sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
max-width: 900px;
width: 90%;
background: white;
padding: 40px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.container:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0,0,0,0.15);
}
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
font-weight: 700;
}
.key-list {
margin-bottom: 30px;
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
transition: all 0.3s ease;
}
.key-list:hover {
background: #e9ecef;
}
.key-list h2 {
color: #2c3e50;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1.5em;
}
ul {
list-style-type: none;
padding: 0;
}
li {
background: white;
border: 1px solid #e0e0e0;
margin-bottom: 10px;
padding: 12px;
border-radius: 5px;
transition: all 0.2s ease;
display: flex;
justify-content: space-between;
align-items: center;
}
li:hover {
transform: translateX(5px);
box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
}
.total {
font-weight: bold;
margin-top: 20px;
text-align: center;
font-size: 1.2em;
color: #2c3e50;
}
.copy-btn {
background-color: #3498db;
color: white;
border: none;
padding: 8px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: background-color 0.3s ease;
}
.copy-btn:hover {
background-color: #2980b9;
}
#copyStatus {
text-align: center;
margin-top: 20px;
color: #27ae60;
font-weight: bold;
opacity: 0;
transition: opacity 0.3s ease;
}
.fail-count {
color: #e74c3c;
font-size: 0.9em;
margin-left: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>API密钥状态</h1>
<div class="key-list">
<h2>
有效密钥
<button class="copy-btn" onclick="copyKeys('valid')">一键复制</button>
</h2>
<ul id="validKeys">
{% for key, fail_count in valid_keys.items() %}
<li>
<span>{{ key }}</span>
<span class="fail-count">(失败次数: {{ fail_count }})</span>
<button class="copy-btn" onclick="copyKey('{{ key }}')">复制</button>
</li>
{% endfor %}
</ul>
</div>
<div class="key-list">
<h2>
无效密钥
<button class="copy-btn" onclick="copyKeys('invalid')">一键复制</button>
</h2>
<ul id="invalidKeys">
{% for key, fail_count in invalid_keys.items() %}
<li>
<span>{{ key }}</span>
<span class="fail-count">(失败次数: {{ fail_count }})</span>
<button class="copy-btn" onclick="copyKey('{{ key }}')">复制</button>
</li>
{% endfor %}
</ul>
</div>
<div class="total">
总密钥数:{{ total }}
</div>
<div id="copyStatus"></div>
</div>
<script>
function copyToClipboard(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text);
} else {
return new Promise((resolve, reject) => {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
resolve();
} else {
reject(new Error('复制失败'));
}
} catch (err) {
document.body.removeChild(textArea);
reject(err);
}
});
}
}
function copyKeys(type) {
const keys = Array.from(document.querySelectorAll(`#${type}Keys li span:first-child`)).map(span => span.textContent.trim());
const jsonKeys = JSON.stringify(keys);
copyToClipboard(jsonKeys)
.then(() => {
showCopyStatus(`已成功复制 ${type === 'valid' ? '有效' : '无效'} 密钥到剪贴板`);
})
.catch((err) => {
console.error('无法复制文本: ', err);
showCopyStatus('复制失败,请重试');
});
}
function copyKey(key) {
copyToClipboard(key)
.then(() => {
showCopyStatus(`已成功复制密钥到剪贴板`);
})
.catch((err) => {
console.error('无法复制文本: ', err);
showCopyStatus('复制失败,请重试');
});
}
function showCopyStatus(message) {
const statusElement = document.getElementById('copyStatus');
statusElement.textContent = message;
statusElement.style.opacity = 1;
setTimeout(() => {
statusElement.style.opacity = 0;
}, 2000);
}
</script>
</body>
</html>
+1
View File
@@ -7,3 +7,4 @@ requests
starlette starlette
uvicorn uvicorn
google-genai google-genai
jinja2