mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-05 15:36:45 +08:00
feat: 添加Web验证页面并优化密钥管理功能
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
+78
-5
@@ -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,12 +68,49 @@ 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"}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
+2
-1
@@ -6,4 +6,5 @@ pydantic_settings
|
|||||||
requests
|
requests
|
||||||
starlette
|
starlette
|
||||||
uvicorn
|
uvicorn
|
||||||
google-genai
|
google-genai
|
||||||
|
jinja2
|
||||||
|
|||||||
Reference in New Issue
Block a user