mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-05-11 18:09:55 +08:00
feat: 添加Web配置编辑器界面
新增 `/config` 路由,提供一个可视化的配置编辑页面 (`config_editor.html`)。 用户现在可以通过网页界面管理: - API 密钥(包括批量添加和重置确认) - API 基础配置 (允许的令牌, 认证令牌, 基础URL, 最大失败次数, 超时) - 模型相关配置 (测试模型, 图像/搜索/过滤模型列表, 代码执行/搜索链接/思考过程开关) - 图像生成配置 (付费密钥, 模型, 上传提供商及相关密钥/URL) - 流式输出优化器配置 (开关, 延迟, 阈值, 分块大小) 同时更新了 `/keys` 页面 (`keys_status.html`): - 页面主标题更改为 "Gemini Balance"。 - 添加了顶部导航选项卡,方便在 "配置编辑" (`/config`) 和 "密钥管理" (`/keys`) 之间切换。
This commit is contained in:
@@ -152,4 +152,8 @@ def get_middleware_logger():
|
||||
|
||||
|
||||
def get_routes_logger():
|
||||
return Logger.setup_logger("routes")
|
||||
return Logger.setup_logger("routes")
|
||||
|
||||
|
||||
def get_config_routes_logger():
|
||||
return Logger.setup_logger("config_routes")
|
||||
48
app/router/config_routes.py
Normal file
48
app/router/config_routes.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
配置路由模块
|
||||
"""
|
||||
from typing import Any, Dict
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from app.core.security import verify_auth_token
|
||||
from app.log.logger import get_config_routes_logger
|
||||
from app.service.config.config_service import ConfigService
|
||||
|
||||
# 创建路由
|
||||
router = APIRouter(prefix="/api/config", tags=["config"])
|
||||
|
||||
logger = get_config_routes_logger()
|
||||
|
||||
|
||||
@router.get("", response_model=Dict[str, Any])
|
||||
async def get_config(request: Request):
|
||||
auth_token = request.cookies.get("auth_token")
|
||||
if not auth_token or not verify_auth_token(auth_token):
|
||||
logger.warning("Unauthorized access attempt to config page")
|
||||
return RedirectResponse(url="/", status_code=302)
|
||||
return ConfigService.get_config()
|
||||
|
||||
|
||||
@router.put("", response_model=Dict[str, Any])
|
||||
async def update_config(config_data: Dict[str, Any], request: Request):
|
||||
auth_token = request.cookies.get("auth_token")
|
||||
if not auth_token or not verify_auth_token(auth_token):
|
||||
logger.warning("Unauthorized access attempt to config page")
|
||||
return RedirectResponse(url="/", status_code=302)
|
||||
try:
|
||||
return ConfigService.update_config(config_data)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/reset", response_model=Dict[str, Any])
|
||||
async def reset_config(request: Request):
|
||||
auth_token = request.cookies.get("auth_token")
|
||||
if not auth_token or not verify_auth_token(auth_token):
|
||||
logger.warning("Unauthorized access attempt to config page")
|
||||
return RedirectResponse(url="/", status_code=302)
|
||||
try:
|
||||
return ConfigService.reset_config()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -8,7 +8,7 @@ from fastapi.templating import Jinja2Templates
|
||||
|
||||
from app.core.security import verify_auth_token
|
||||
from app.log.logger import get_routes_logger
|
||||
from app.router import gemini_routes, openai_routes
|
||||
from app.router import gemini_routes, openai_routes, config_routes
|
||||
from app.service.key.key_manager import get_key_manager_instance
|
||||
|
||||
logger = get_routes_logger()
|
||||
@@ -28,6 +28,7 @@ def setup_routers(app: FastAPI) -> None:
|
||||
app.include_router(openai_routes.router)
|
||||
app.include_router(gemini_routes.router)
|
||||
app.include_router(gemini_routes.router_v1beta)
|
||||
app.include_router(config_routes.router)
|
||||
|
||||
# 添加页面路由
|
||||
setup_page_routes(app)
|
||||
@@ -97,6 +98,21 @@ def setup_page_routes(app: FastAPI) -> None:
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving keys status: {str(e)}")
|
||||
raise
|
||||
|
||||
@app.get("/config", response_class=HTMLResponse)
|
||||
async def config_page(request: Request):
|
||||
"""配置编辑页面"""
|
||||
try:
|
||||
auth_token = request.cookies.get("auth_token")
|
||||
if not auth_token or not verify_auth_token(auth_token):
|
||||
logger.warning("Unauthorized access attempt to config page")
|
||||
return RedirectResponse(url="/", status_code=302)
|
||||
|
||||
logger.info("Config page accessed successfully")
|
||||
return templates.TemplateResponse("config_editor.html", {"request": request})
|
||||
except Exception as e:
|
||||
logger.error(f"Error accessing config page: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
def setup_health_routes(app: FastAPI) -> None:
|
||||
|
||||
104
app/service/config/config_service.py
Normal file
104
app/service/config/config_service.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
配置服务模块
|
||||
"""
|
||||
import os
|
||||
from typing import Any, Dict
|
||||
import json
|
||||
from dotenv import load_dotenv, set_key
|
||||
|
||||
from app.config.config import settings, Settings
|
||||
|
||||
|
||||
class ConfigService:
|
||||
"""配置服务类,用于管理应用程序配置"""
|
||||
|
||||
@staticmethod
|
||||
def get_config() -> Dict[str, Any]:
|
||||
"""
|
||||
获取当前配置
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 配置字典
|
||||
"""
|
||||
config_dict = {}
|
||||
|
||||
# 获取Settings类的所有字段
|
||||
for field_name in settings.model_fields:
|
||||
value = getattr(settings, field_name)
|
||||
config_dict[field_name] = value
|
||||
|
||||
return config_dict
|
||||
|
||||
@staticmethod
|
||||
def update_config(config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
更新配置
|
||||
|
||||
Args:
|
||||
config_data (Dict[str, Any]): 新的配置数据
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 更新后的配置字典
|
||||
"""
|
||||
# 更新settings对象
|
||||
for key, value in config_data.items():
|
||||
if hasattr(settings, key):
|
||||
setattr(settings, key, value)
|
||||
|
||||
# 更新.env文件
|
||||
ConfigService._update_env_file(config_data)
|
||||
|
||||
return ConfigService.get_config()
|
||||
|
||||
@staticmethod
|
||||
def reset_config() -> Dict[str, Any]:
|
||||
"""
|
||||
重置配置到默认值
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 重置后的配置字典
|
||||
"""
|
||||
# 重新加载.env文件
|
||||
load_dotenv(override=True)
|
||||
|
||||
# 重新创建settings对象
|
||||
global settings
|
||||
settings = Settings()
|
||||
|
||||
return ConfigService.get_config()
|
||||
|
||||
@staticmethod
|
||||
def _update_env_file(config_data: Dict[str, Any]) -> None:
|
||||
"""
|
||||
更新.env文件
|
||||
|
||||
Args:
|
||||
config_data (Dict[str, Any]): 配置数据
|
||||
"""
|
||||
env_path = ".env"
|
||||
|
||||
# 确保.env文件存在
|
||||
if not os.path.exists(env_path):
|
||||
# 如果不存在,复制.env.example
|
||||
if os.path.exists(".env.example"):
|
||||
with open(".env.example", "r", encoding="utf-8") as example_file:
|
||||
with open(env_path, "w", encoding="utf-8") as env_file:
|
||||
env_file.write(example_file.read())
|
||||
else:
|
||||
# 创建空文件
|
||||
open(env_path, "w", encoding="utf-8").close()
|
||||
|
||||
# 更新.env文件中的配置
|
||||
for key, value in config_data.items():
|
||||
# 处理不同类型的值
|
||||
if isinstance(value, list):
|
||||
# 将列表转换为JSON字符串
|
||||
env_value = json.dumps(value)
|
||||
elif isinstance(value, bool):
|
||||
# 布尔值转换为小写字符串
|
||||
env_value = str(value).lower()
|
||||
else:
|
||||
env_value = str(value)
|
||||
|
||||
# 更新.env文件
|
||||
set_key(env_path, key, env_value)
|
||||
792
app/static/css/config_editor.css
Normal file
792
app/static/css/config_editor.css
Normal file
@@ -0,0 +1,792 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
width: 95%;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 40px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 15px 35px rgba(0,0,0,0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
position: relative;
|
||||
margin: 20px auto;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 40px);
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
position: relative;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
h1::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100px;
|
||||
height: 4px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5em;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 导航标签样式 */
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 2px solid rgba(0,0,0,0.1);
|
||||
padding-bottom: 10px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.tab-link {
|
||||
color: #2c3e50;
|
||||
text-decoration: none;
|
||||
padding: 12px 25px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
margin: 0 5px;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tab-link:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.tab-link.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
|
||||
}
|
||||
|
||||
.tab-link.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -12px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tab-link i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.config-tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
padding: 10px 20px;
|
||||
border-radius: 30px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
color: #2c3e50;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
|
||||
}
|
||||
|
||||
.config-section {
|
||||
display: none;
|
||||
animation: fadeIn 0.5s ease forwards;
|
||||
background: rgba(248, 249, 250, 0.9);
|
||||
padding: 25px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.config-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
margin-bottom: 25px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.config-item label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.config-item input[type="text"],
|
||||
.config-item input[type="number"],
|
||||
.config-item select {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.config-item input[type="text"]:focus,
|
||||
.config-item input[type="number"]:focus,
|
||||
.config-item select:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.array-container {
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
/* margin-bottom: 10px; */ /* Removed as controls are now outside */
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
/* Specific style for API Keys container to make it scrollable */
|
||||
#API_KEYS_container {
|
||||
max-height: 300px; /* Adjust this value as needed */
|
||||
overflow-y: auto;
|
||||
/* Optional: Add some padding to the right for the scrollbar */
|
||||
padding-right: 5px;
|
||||
margin-bottom: 10px; /* Add margin below the container */
|
||||
}
|
||||
|
||||
/* Search Input Styles */
|
||||
.search-container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#apiKeySearchInput {
|
||||
width: 100%;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#apiKeySearchInput:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
.array-item {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
gap: 10px;
|
||||
background-color: rgba(248, 249, 250, 0.5);
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed rgba(102, 126, 234, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.array-item:hover {
|
||||
background-color: rgba(248, 249, 250, 0.8);
|
||||
border-color: rgba(102, 126, 234, 0.5);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.array-item input {
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
padding: 12px 15px;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.array-item input:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.array-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px; /* Increase margin-top for spacing */
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 15px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
border: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.remove-btn:hover {
|
||||
background: #c0392b;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.toggle label {
|
||||
margin-bottom: 0;
|
||||
flex: 1;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
input:focus + .toggle-slider {
|
||||
box-shadow: 0 0 1px #667eea;
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.save-btn, .reset-btn {
|
||||
padding: 12px 30px;
|
||||
border-radius: 30px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
border: none;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.save-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 20px rgba(118, 75, 162, 0.3);
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||
}
|
||||
|
||||
.save-btn:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 5px 10px rgba(118, 75, 162, 0.2);
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: linear-gradient(135deg, #e0e0e0 0%, #bdc3c7 100%);
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
background: linear-gradient(135deg, #bdc3c7 0%, #e0e0e0 100%);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.reset-btn:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 5px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.save-status {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(39, 174, 96, 0.9);
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.save-status.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.save-status.error {
|
||||
background: rgba(231, 76, 60, 0.9);
|
||||
}
|
||||
|
||||
.provider-config {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.provider-config.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.notification {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 25px;
|
||||
border-radius: 8px;
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.notification.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
background: rgba(39, 174, 96, 0.9);
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
background: rgba(231, 76, 60, 0.9);
|
||||
}
|
||||
|
||||
.scroll-buttons {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.scroll-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(5px);
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.scroll-btn:hover {
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.scroll-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 8px 20px rgba(118, 75, 162, 0.3);
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||
}
|
||||
|
||||
.refresh-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.refresh-btn i {
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
.refresh-btn.loading i {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #2c3e50;
|
||||
backdrop-filter: blur(5px);
|
||||
border-top: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.copyright a {
|
||||
color: #764ba2;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.copyright a:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.copyright img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
display: none; /* Hidden by default */
|
||||
position: fixed; /* Stay in place */
|
||||
z-index: 1001; /* Sit on top */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%; /* Full width */
|
||||
height: 100%; /* Full height */
|
||||
overflow: auto; /* Enable scroll if needed */
|
||||
background-color: rgba(0,0,0,0.6); /* Black w/ opacity */
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
padding: 30px;
|
||||
border: 1px solid #888;
|
||||
width: 80%; /* Could be more or less, depending on screen size */
|
||||
max-width: 600px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
position: relative;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
margin-top: 0;
|
||||
color: #2c3e50;
|
||||
border-bottom: none; /* Remove border from h2 inside modal */
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.modal-content p {
|
||||
color: #7f8c8d;
|
||||
font-size: 14px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.modal-content textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-family: monospace; /* Use monospace for keys */
|
||||
font-size: 14px;
|
||||
resize: vertical; /* Allow vertical resizing */
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
color: #aaa;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 20px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.close-btn:hover,
|
||||
.close-btn:focus {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateY(-50px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
.nav-tabs {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.tab-link {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.tab-link.active::after {
|
||||
display: none;
|
||||
}
|
||||
.config-tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
.tab-btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.toggle {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
.save-btn, .reset-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.scroll-buttons {
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
}
|
||||
.scroll-btn {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.refresh-btn {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: 15px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
.config-section {
|
||||
padding: 15px;
|
||||
}
|
||||
.config-item input[type="text"],
|
||||
.config-item input[type="number"],
|
||||
.config-item select {
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ body {
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
position: relative;
|
||||
@@ -49,6 +49,61 @@ h1::after {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 导航标签样式 */
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 2px solid rgba(0,0,0,0.1);
|
||||
padding-bottom: 10px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.tab-link {
|
||||
color: #2c3e50;
|
||||
text-decoration: none;
|
||||
padding: 12px 25px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
margin: 0 5px;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tab-link:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.tab-link.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
|
||||
}
|
||||
|
||||
.tab-link.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -12px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tab-link i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.key-list {
|
||||
margin-bottom: 30px;
|
||||
background: rgba(248, 249, 250, 0.9);
|
||||
@@ -161,27 +216,34 @@ li:hover {
|
||||
}
|
||||
|
||||
.verify-btn, .copy-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 15px;
|
||||
border-radius: 8px;
|
||||
border-radius: 30px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.verify-btn {
|
||||
background: linear-gradient(135deg, #2ecc71, #27ae60);
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.verify-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(46, 204, 113, 0.3);
|
||||
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||
}
|
||||
|
||||
.verify-btn:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 5px 10px rgba(118, 75, 162, 0.2);
|
||||
}
|
||||
|
||||
.verify-btn:disabled {
|
||||
@@ -195,13 +257,19 @@ li:hover {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||
}
|
||||
|
||||
.copy-btn:active {
|
||||
transform: translateY(0);
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 5px 10px rgba(118, 75, 162, 0.2);
|
||||
}
|
||||
|
||||
.copy-btn i {
|
||||
@@ -393,6 +461,19 @@ li:hover {
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
.nav-tabs {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.tab-link {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.tab-link.active::after {
|
||||
display: none;
|
||||
}
|
||||
.key-list h2 {
|
||||
font-size: 1.2em;
|
||||
flex-direction: column;
|
||||
|
||||
555
app/static/js/config_editor.js
Normal file
555
app/static/js/config_editor.js
Normal file
@@ -0,0 +1,555 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化配置
|
||||
initConfig();
|
||||
|
||||
// 标签切换
|
||||
const tabButtons = document.querySelectorAll('.tab-btn');
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
// 防止事件冒泡
|
||||
e.stopPropagation();
|
||||
const tabId = this.getAttribute('data-tab');
|
||||
switchTab(tabId);
|
||||
});
|
||||
});
|
||||
|
||||
// 上传提供商切换
|
||||
const uploadProviderSelect = document.getElementById('UPLOAD_PROVIDER');
|
||||
if (uploadProviderSelect) {
|
||||
uploadProviderSelect.addEventListener('change', function() {
|
||||
toggleProviderConfig(this.value);
|
||||
});
|
||||
}
|
||||
|
||||
// 切换按钮事件
|
||||
const toggleSwitches = document.querySelectorAll('.toggle-switch');
|
||||
toggleSwitches.forEach(toggleSwitch => {
|
||||
toggleSwitch.addEventListener('click', function(e) {
|
||||
// 防止事件冒泡
|
||||
e.stopPropagation();
|
||||
const checkbox = this.querySelector('input[type="checkbox"]');
|
||||
if (checkbox) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 保存按钮
|
||||
const saveBtn = document.getElementById('saveBtn');
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', saveConfig);
|
||||
}
|
||||
|
||||
// 重置按钮
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', resetConfig);
|
||||
}
|
||||
|
||||
// 滚动按钮
|
||||
window.addEventListener('scroll', toggleScrollButtons);
|
||||
|
||||
// --- 新增:API Key 模态框和搜索相关 ---
|
||||
const apiKeyModal = document.getElementById('apiKeyModal');
|
||||
const addApiKeyBtn = document.getElementById('addApiKeyBtn');
|
||||
const closeApiKeyModalBtn = document.getElementById('closeApiKeyModalBtn');
|
||||
const cancelAddApiKeyBtn = document.getElementById('cancelAddApiKeyBtn');
|
||||
const confirmAddApiKeyBtn = document.getElementById('confirmAddApiKeyBtn');
|
||||
const apiKeyBulkInput = document.getElementById('apiKeyBulkInput');
|
||||
const apiKeySearchInput = document.getElementById('apiKeySearchInput');
|
||||
|
||||
// --- 新增:重置确认模态框相关 ---
|
||||
const resetConfirmModal = document.getElementById('resetConfirmModal');
|
||||
const closeResetModalBtn = document.getElementById('closeResetModalBtn');
|
||||
const cancelResetBtn = document.getElementById('cancelResetBtn');
|
||||
const confirmResetBtn = document.getElementById('confirmResetBtn');
|
||||
// --- 结束:新增 ---
|
||||
|
||||
|
||||
// 打开模态框
|
||||
if (addApiKeyBtn) {
|
||||
addApiKeyBtn.addEventListener('click', () => {
|
||||
if (apiKeyModal) {
|
||||
apiKeyModal.classList.add('show');
|
||||
}
|
||||
if (apiKeyBulkInput) apiKeyBulkInput.value = ''; // 清空输入框
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭模态框 (X 按钮)
|
||||
if (closeApiKeyModalBtn) {
|
||||
closeApiKeyModalBtn.addEventListener('click', () => {
|
||||
if (apiKeyModal) {
|
||||
apiKeyModal.classList.remove('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭模态框 (取消按钮)
|
||||
if (cancelAddApiKeyBtn) {
|
||||
cancelAddApiKeyBtn.addEventListener('click', () => {
|
||||
if (apiKeyModal) {
|
||||
apiKeyModal.classList.remove('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 点击模态框外部关闭 (处理两个模态框)
|
||||
window.addEventListener('click', (event) => {
|
||||
if (event.target == apiKeyModal) {
|
||||
apiKeyModal.classList.remove('show');
|
||||
}
|
||||
if (event.target == resetConfirmModal) { // 新增对重置模态框的处理
|
||||
resetConfirmModal.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// 确认添加 API Key
|
||||
if (confirmAddApiKeyBtn) {
|
||||
confirmAddApiKeyBtn.addEventListener('click', handleBulkAddApiKeys);
|
||||
}
|
||||
|
||||
// API Key 搜索 (稍后实现具体逻辑)
|
||||
if (apiKeySearchInput) {
|
||||
apiKeySearchInput.addEventListener('input', handleApiKeySearch);
|
||||
}
|
||||
// --- 结束:API Key 相关 ---
|
||||
|
||||
// --- 新增:重置确认模态框事件监听 (移到 DOMContentLoaded 内部) ---
|
||||
if (closeResetModalBtn) {
|
||||
closeResetModalBtn.addEventListener('click', () => {
|
||||
if (resetConfirmModal) {
|
||||
resetConfirmModal.classList.remove('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
if (cancelResetBtn) {
|
||||
cancelResetBtn.addEventListener('click', () => {
|
||||
if (resetConfirmModal) {
|
||||
resetConfirmModal.classList.remove('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
if (confirmResetBtn) {
|
||||
// 调用之前定义的 executeReset 函数
|
||||
confirmResetBtn.addEventListener('click', () => {
|
||||
if (resetConfirmModal) {
|
||||
resetConfirmModal.classList.remove('show'); // 关闭模态框
|
||||
}
|
||||
executeReset(); // 执行重置逻辑
|
||||
});
|
||||
}
|
||||
// --- 结束:重置相关 ---
|
||||
|
||||
}); // <-- DOMContentLoaded 结束括号
|
||||
|
||||
// 初始化配置
|
||||
async function initConfig() {
|
||||
try {
|
||||
showNotification('正在加载配置...', 'info');
|
||||
const response = await fetch('/api/config');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const config = await response.json();
|
||||
|
||||
// 确保数组字段有默认值
|
||||
if (!config.API_KEYS || !Array.isArray(config.API_KEYS) || config.API_KEYS.length === 0) {
|
||||
config.API_KEYS = ['请在此处输入 API 密钥'];
|
||||
}
|
||||
|
||||
if (!config.ALLOWED_TOKENS || !Array.isArray(config.ALLOWED_TOKENS) || config.ALLOWED_TOKENS.length === 0) {
|
||||
config.ALLOWED_TOKENS = [''];
|
||||
}
|
||||
|
||||
if (!config.IMAGE_MODELS || !Array.isArray(config.IMAGE_MODELS) || config.IMAGE_MODELS.length === 0) {
|
||||
config.IMAGE_MODELS = ['gemini-1.5-pro-latest'];
|
||||
}
|
||||
|
||||
if (!config.SEARCH_MODELS || !Array.isArray(config.SEARCH_MODELS) || config.SEARCH_MODELS.length === 0) {
|
||||
config.SEARCH_MODELS = ['gemini-1.5-flash-latest'];
|
||||
}
|
||||
|
||||
if (!config.FILTERED_MODELS || !Array.isArray(config.FILTERED_MODELS) || config.FILTERED_MODELS.length === 0) {
|
||||
config.FILTERED_MODELS = ['gemini-1.0-pro-latest'];
|
||||
}
|
||||
|
||||
populateForm(config);
|
||||
|
||||
// 确保上传提供商有默认值
|
||||
const uploadProvider = document.getElementById('UPLOAD_PROVIDER');
|
||||
if (uploadProvider && !uploadProvider.value) {
|
||||
uploadProvider.value = 'smms'; // 设置默认值为 smms
|
||||
toggleProviderConfig('smms');
|
||||
}
|
||||
|
||||
showNotification('配置加载成功', 'success');
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error);
|
||||
showNotification('加载配置失败: ' + error.message, 'error');
|
||||
|
||||
// 加载失败时,使用默认配置
|
||||
const defaultConfig = {
|
||||
API_KEYS: [''],
|
||||
ALLOWED_TOKENS: [''],
|
||||
IMAGE_MODELS: ['gemini-1.5-pro-latest'],
|
||||
SEARCH_MODELS: ['gemini-1.5-flash-latest'],
|
||||
FILTERED_MODELS: ['gemini-1.0-pro-latest'],
|
||||
UPLOAD_PROVIDER: 'smms'
|
||||
};
|
||||
|
||||
populateForm(defaultConfig);
|
||||
toggleProviderConfig('smms');
|
||||
}
|
||||
}
|
||||
|
||||
// 填充表单
|
||||
function populateForm(config) {
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
// 首先检查是否是数组类型
|
||||
if (Array.isArray(value)) {
|
||||
const container = document.getElementById(`${key}_container`);
|
||||
if (container) {
|
||||
// 清除现有项
|
||||
const existingItems = container.querySelectorAll('.array-item');
|
||||
existingItems.forEach(item => item.remove());
|
||||
// 添加数组项
|
||||
value.forEach(item => {
|
||||
// 确保只添加非空字符串项(如果需要)
|
||||
// if (item && typeof item === 'string' && item.trim() !== '') {
|
||||
addArrayItemWithValue(key, item);
|
||||
// }
|
||||
});
|
||||
}
|
||||
// 处理完数组后,跳过本次循环的剩余部分
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果不是数组,再尝试查找对应的单个元素
|
||||
const element = document.getElementById(key);
|
||||
if (element) {
|
||||
if (typeof value === 'boolean') {
|
||||
element.checked = value;
|
||||
} else {
|
||||
// 处理其他类型 (确保 value 不是 null 或 undefined)
|
||||
element.value = value ?? ''; // 使用空字符串作为默认值
|
||||
}
|
||||
}
|
||||
// 如果既不是数组,也找不到对应 ID 的元素,则忽略该配置项
|
||||
}
|
||||
|
||||
// 初始化上传提供商配置 (保持不变)
|
||||
const uploadProvider = document.getElementById('UPLOAD_PROVIDER');
|
||||
if (uploadProvider) {
|
||||
toggleProviderConfig(uploadProvider.value);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 新增:处理批量添加 API Key 的逻辑 ---
|
||||
function handleBulkAddApiKeys() {
|
||||
const apiKeyBulkInput = document.getElementById('apiKeyBulkInput');
|
||||
const apiKeyContainer = document.getElementById('API_KEYS_container');
|
||||
const apiKeyModal = document.getElementById('apiKeyModal');
|
||||
|
||||
if (!apiKeyBulkInput || !apiKeyContainer || !apiKeyModal) return;
|
||||
|
||||
const bulkText = apiKeyBulkInput.value;
|
||||
const keyRegex = /AIzaSy\S{33}/g; // 全局匹配
|
||||
const extractedKeys = bulkText.match(keyRegex) || [];
|
||||
|
||||
// 获取当前已有的 keys
|
||||
const currentKeyInputs = apiKeyContainer.querySelectorAll('.array-input');
|
||||
const currentKeys = Array.from(currentKeyInputs).map(input => input.value).filter(key => key.trim() !== '');
|
||||
|
||||
// 合并并去重
|
||||
const combinedKeys = new Set([...currentKeys, ...extractedKeys]);
|
||||
const uniqueKeys = Array.from(combinedKeys);
|
||||
|
||||
// 清空现有列表显示
|
||||
const existingItems = apiKeyContainer.querySelectorAll('.array-item');
|
||||
existingItems.forEach(item => item.remove());
|
||||
|
||||
// 重新填充列表
|
||||
uniqueKeys.forEach(key => {
|
||||
addArrayItemWithValue('API_KEYS', key);
|
||||
});
|
||||
|
||||
// 关闭模态框
|
||||
apiKeyModal.classList.remove('show');
|
||||
showNotification(`添加/更新了 ${uniqueKeys.length} 个唯一密钥`, 'success');
|
||||
}
|
||||
|
||||
// --- 新增:处理 API Key 搜索的逻辑 ---
|
||||
function handleApiKeySearch() {
|
||||
const apiKeySearchInput = document.getElementById('apiKeySearchInput');
|
||||
const apiKeyContainer = document.getElementById('API_KEYS_container');
|
||||
|
||||
if (!apiKeySearchInput || !apiKeyContainer) return;
|
||||
|
||||
const searchTerm = apiKeySearchInput.value.toLowerCase();
|
||||
const keyItems = apiKeyContainer.querySelectorAll('.array-item');
|
||||
|
||||
keyItems.forEach(item => {
|
||||
const input = item.querySelector('.array-input');
|
||||
if (input) {
|
||||
const key = input.value.toLowerCase();
|
||||
if (key.includes(searchTerm)) {
|
||||
item.style.display = 'flex'; // 或者 'block',取决于你的布局
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换标签
|
||||
function switchTab(tabId) {
|
||||
// 更新标签按钮状态
|
||||
const tabButtons = document.querySelectorAll('.tab-btn');
|
||||
tabButtons.forEach(button => {
|
||||
if (button.getAttribute('data-tab') === tabId) {
|
||||
button.classList.add('active');
|
||||
} else {
|
||||
button.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 更新内容区域
|
||||
const sections = document.querySelectorAll('.config-section');
|
||||
sections.forEach(section => {
|
||||
if (section.id === `${tabId}-section`) {
|
||||
section.classList.add('active');
|
||||
} else {
|
||||
section.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换上传提供商配置
|
||||
function toggleProviderConfig(provider) {
|
||||
const providerConfigs = document.querySelectorAll('.provider-config');
|
||||
providerConfigs.forEach(config => {
|
||||
if (config.getAttribute('data-provider') === provider) {
|
||||
config.classList.add('active');
|
||||
} else {
|
||||
config.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 添加数组项
|
||||
function addArrayItem(key) {
|
||||
const container = document.getElementById(`${key}_container`);
|
||||
if (!container) return;
|
||||
|
||||
addArrayItemWithValue(key, '');
|
||||
}
|
||||
|
||||
// 添加带值的数组项
|
||||
function addArrayItemWithValue(key, value) {
|
||||
const container = document.getElementById(`${key}_container`);
|
||||
if (!container) return;
|
||||
|
||||
const arrayItem = document.createElement('div');
|
||||
arrayItem.className = 'array-item';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.name = `${key}[]`;
|
||||
input.value = value;
|
||||
input.className = 'array-input';
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'remove-btn';
|
||||
removeBtn.innerHTML = '<i class="fas fa-times"></i>';
|
||||
removeBtn.addEventListener('click', function() {
|
||||
arrayItem.remove();
|
||||
});
|
||||
|
||||
arrayItem.appendChild(input);
|
||||
arrayItem.appendChild(removeBtn);
|
||||
|
||||
// 插入到添加按钮之前
|
||||
const controls = container.querySelector('.array-controls');
|
||||
container.insertBefore(arrayItem, controls);
|
||||
}
|
||||
|
||||
// 收集表单数据
|
||||
function collectFormData() {
|
||||
const formData = {};
|
||||
|
||||
// 处理普通输入
|
||||
const inputs = document.querySelectorAll('input[type="text"], input[type="number"], select');
|
||||
inputs.forEach(input => {
|
||||
if (!input.name.includes('[]')) {
|
||||
if (input.type === 'number') {
|
||||
formData[input.name] = parseFloat(input.value);
|
||||
} else {
|
||||
formData[input.name] = input.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 处理复选框
|
||||
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
formData[checkbox.name] = checkbox.checked;
|
||||
});
|
||||
|
||||
// 处理数组
|
||||
const arrayContainers = document.querySelectorAll('.array-container');
|
||||
arrayContainers.forEach(container => {
|
||||
const key = container.id.replace('_container', '');
|
||||
const arrayInputs = container.querySelectorAll('.array-input');
|
||||
formData[key] = Array.from(arrayInputs).map(input => input.value).filter(value => value.trim() !== '');
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function saveConfig() {
|
||||
try {
|
||||
const formData = collectFormData();
|
||||
|
||||
showNotification('正在保存配置...', 'info');
|
||||
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// 显示保存状态
|
||||
const saveStatus = document.getElementById('saveStatus');
|
||||
saveStatus.classList.add('show');
|
||||
|
||||
setTimeout(() => {
|
||||
saveStatus.classList.remove('show');
|
||||
}, 3000);
|
||||
|
||||
showNotification('配置保存成功', 'success');
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error);
|
||||
|
||||
// 显示错误状态
|
||||
const saveStatus = document.getElementById('saveStatus');
|
||||
saveStatus.classList.add('show', 'error');
|
||||
saveStatus.querySelector('.status-icon i').className = 'fas fa-times-circle';
|
||||
saveStatus.querySelector('.status-text').textContent = '配置保存失败';
|
||||
|
||||
setTimeout(() => {
|
||||
saveStatus.classList.remove('show', 'error');
|
||||
}, 3000);
|
||||
|
||||
showNotification('保存配置失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 重置配置 (现在只负责打开模态框)
|
||||
function resetConfig(event) {
|
||||
// 阻止事件冒泡和默认行为
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
console.log('resetConfig called. Event target:', event ? event.target.id : 'No event');
|
||||
|
||||
// 确保只有当事件来自重置按钮时才显示模态框
|
||||
if (!event || event.target.id === 'resetBtn' || event.currentTarget.id === 'resetBtn') {
|
||||
const resetConfirmModal = document.getElementById('resetConfirmModal');
|
||||
if (resetConfirmModal) {
|
||||
resetConfirmModal.classList.add('show');
|
||||
} else {
|
||||
// Fallback if modal doesn't exist for some reason
|
||||
console.error("Reset confirmation modal not found! Falling back to default confirm.");
|
||||
// Fallback to original confirm behavior
|
||||
if (!confirm('确定要重置所有配置吗?这将恢复到默认值。')) {
|
||||
return;
|
||||
}
|
||||
// If confirmed, proceed with the reset logic directly (less ideal)
|
||||
executeReset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 新增:将实际重置逻辑提取到一个单独的函数 ---
|
||||
async function executeReset() {
|
||||
try {
|
||||
showNotification('正在重置配置...', 'info');
|
||||
const response = await fetch('/api/config/reset', { method: 'POST' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const config = await response.json();
|
||||
populateForm(config);
|
||||
showNotification('配置已重置为默认值', 'success');
|
||||
} catch (error) {
|
||||
console.error('重置配置失败:', error);
|
||||
showNotification('重置配置失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示通知
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.getElementById('notification');
|
||||
notification.textContent = message;
|
||||
notification.className = 'notification show';
|
||||
|
||||
if (type) {
|
||||
notification.classList.add(type);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 刷新页面
|
||||
function refreshPage(button) {
|
||||
button.classList.add('loading');
|
||||
location.reload();
|
||||
}
|
||||
|
||||
// 滚动到顶部
|
||||
function scrollToTop() {
|
||||
const container = document.querySelector('.container');
|
||||
container.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
function scrollToBottom() {
|
||||
const container = document.querySelector('.container');
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
// 切换滚动按钮显示
|
||||
function toggleScrollButtons() {
|
||||
const scrollButtons = document.querySelector('.scroll-buttons');
|
||||
|
||||
if (window.scrollY > 200) {
|
||||
scrollButtons.style.display = 'flex';
|
||||
} else {
|
||||
scrollButtons.style.display = 'none';
|
||||
}
|
||||
}
|
||||
327
app/templates/config_editor.html
Normal file
327
app/templates/config_editor.html
Normal file
@@ -0,0 +1,327 @@
|
||||
<!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 rel="manifest" href="/static/manifest.json">
|
||||
<meta name="theme-color" content="#764ba2">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="GBalance">
|
||||
<link rel="icon" href="/static/icons/icon-192x192.png">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/config_editor.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<button class="refresh-btn" onclick="refreshPage(this)">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
<h1>Gemini Balance</h1>
|
||||
<div class="nav-tabs">
|
||||
<a href="/config" class="tab-link active">
|
||||
<i class="fas fa-cog"></i> 配置编辑
|
||||
</a>
|
||||
<a href="/keys" class="tab-link">
|
||||
<i class="fas fa-key"></i> 密钥管理
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="config-tabs">
|
||||
<button class="tab-btn active" data-tab="api">API配置</button>
|
||||
<button class="tab-btn" data-tab="model">模型配置</button>
|
||||
<button class="tab-btn" data-tab="image">图像生成</button>
|
||||
<button class="tab-btn" data-tab="stream">流式输出</button>
|
||||
</div>
|
||||
|
||||
<div class="save-status" id="saveStatus">
|
||||
<span class="status-icon"><i class="fas fa-check-circle"></i></span>
|
||||
<span class="status-text">配置已保存</span>
|
||||
</div>
|
||||
|
||||
<form id="configForm">
|
||||
<!-- API相关配置 -->
|
||||
<div class="config-section active" id="api-section">
|
||||
<h2><i class="fas fa-key"></i> API相关配置</h2>
|
||||
|
||||
<div class="config-item array-input">
|
||||
<label for="API_KEYS">API密钥列表</label>
|
||||
<div class="search-container">
|
||||
<input type="search" id="apiKeySearchInput" placeholder="搜索密钥...">
|
||||
</div>
|
||||
<div class="array-container" id="API_KEYS_container">
|
||||
<!-- 数组项将在这里动态添加 -->
|
||||
</div>
|
||||
<div class="array-controls">
|
||||
<button type="button" class="add-btn" id="addApiKeyBtn">
|
||||
<i class="fas fa-plus"></i> 添加密钥
|
||||
</button>
|
||||
</div>
|
||||
<small class="help-text">Gemini API密钥列表,每行一个</small>
|
||||
</div>
|
||||
|
||||
<!-- API Key Add Modal -->
|
||||
<div id="apiKeyModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-btn" id="closeApiKeyModalBtn">×</span>
|
||||
<h2>批量添加 API 密钥</h2>
|
||||
<p>每行粘贴一个或多个密钥,将自动提取有效密钥并去重。</p>
|
||||
<textarea id="apiKeyBulkInput" rows="10" placeholder="在此处粘贴 API 密钥..."></textarea>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="confirmAddApiKeyBtn" class="save-btn">确认添加</button>
|
||||
<button type="button" id="cancelAddApiKeyBtn" class="reset-btn">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reset Confirmation Modal -->
|
||||
<div id="resetConfirmModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-btn" id="closeResetModalBtn">×</span>
|
||||
<h2>确认重置配置</h2>
|
||||
<p>确定要重置所有配置吗?<br>这将恢复到默认值,此操作不可撤销。</p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="confirmResetBtn" class="reset-btn">确认重置</button>
|
||||
<button type="button" id="cancelResetBtn" class="save-btn">取消</button> <!-- Using save-btn style for cancel -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-item array-input">
|
||||
<label for="ALLOWED_TOKENS">允许的令牌列表</label>
|
||||
<div class="array-container" id="ALLOWED_TOKENS_container">
|
||||
<!-- 数组项将在这里动态添加 -->
|
||||
</div>
|
||||
<div class="array-controls">
|
||||
<button type="button" class="add-btn" onclick="addArrayItem('ALLOWED_TOKENS')">
|
||||
<i class="fas fa-plus"></i> 添加令牌
|
||||
</button>
|
||||
</div>
|
||||
<small class="help-text">允许访问API的令牌列表</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item">
|
||||
<label for="AUTH_TOKEN">认证令牌</label>
|
||||
<input type="text" id="AUTH_TOKEN" name="AUTH_TOKEN" placeholder="默认使用ALLOWED_TOKENS中的第一个">
|
||||
<small class="help-text">用于API认证的令牌</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item">
|
||||
<label for="BASE_URL">API基础URL</label>
|
||||
<input type="text" id="BASE_URL" name="BASE_URL" placeholder="https://generativelanguage.googleapis.com/v1beta">
|
||||
<small class="help-text">Gemini API的基础URL</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item">
|
||||
<label for="MAX_FAILURES">最大失败次数</label>
|
||||
<input type="number" id="MAX_FAILURES" name="MAX_FAILURES" min="1" max="100">
|
||||
<small class="help-text">API密钥失败后标记为无效的次数</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item">
|
||||
<label for="TIME_OUT">请求超时时间(秒)</label>
|
||||
<input type="number" id="TIME_OUT" name="TIME_OUT" min="1" max="600">
|
||||
<small class="help-text">API请求的超时时间</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型相关配置 -->
|
||||
<div class="config-section" id="model-section">
|
||||
<h2><i class="fas fa-robot"></i> 模型相关配置</h2>
|
||||
|
||||
<div class="config-item">
|
||||
<label for="TEST_MODEL">测试模型</label>
|
||||
<input type="text" id="TEST_MODEL" name="TEST_MODEL" placeholder="gemini-1.5-flash">
|
||||
<small class="help-text">用于测试API密钥的模型</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item array-input">
|
||||
<label for="IMAGE_MODELS">图像模型列表</label>
|
||||
<div class="array-container" id="IMAGE_MODELS_container">
|
||||
<!-- 数组项将在这里动态添加 -->
|
||||
<div class="array-controls">
|
||||
<button type="button" class="add-btn" onclick="addArrayItem('IMAGE_MODELS')">
|
||||
<i class="fas fa-plus"></i> 添加模型
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<small class="help-text">支持图像处理的模型列表</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item array-input">
|
||||
<label for="SEARCH_MODELS">搜索模型列表</label>
|
||||
<div class="array-container" id="SEARCH_MODELS_container">
|
||||
<!-- 数组项将在这里动态添加 -->
|
||||
<div class="array-controls">
|
||||
<button type="button" class="add-btn" onclick="addArrayItem('SEARCH_MODELS')">
|
||||
<i class="fas fa-plus"></i> 添加模型
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<small class="help-text">支持搜索功能的模型列表</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item array-input">
|
||||
<label for="FILTERED_MODELS">过滤模型列表</label>
|
||||
<div class="array-container" id="FILTERED_MODELS_container">
|
||||
<!-- 数组项将在这里动态添加 -->
|
||||
<div class="array-controls">
|
||||
<button type="button" class="add-btn" onclick="addArrayItem('FILTERED_MODELS')">
|
||||
<i class="fas fa-plus"></i> 添加模型
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<small class="help-text">需要过滤的模型列表</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item toggle">
|
||||
<label for="TOOLS_CODE_EXECUTION_ENABLED">启用代码执行工具</label>
|
||||
<div class="toggle-switch">
|
||||
<input type="checkbox" id="TOOLS_CODE_EXECUTION_ENABLED" name="TOOLS_CODE_EXECUTION_ENABLED">
|
||||
<span class="toggle-slider"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-item toggle">
|
||||
<label for="SHOW_SEARCH_LINK">显示搜索链接</label>
|
||||
<div class="toggle-switch">
|
||||
<input type="checkbox" id="SHOW_SEARCH_LINK" name="SHOW_SEARCH_LINK">
|
||||
<span class="toggle-slider"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-item toggle">
|
||||
<label for="SHOW_THINKING_PROCESS">显示思考过程</label>
|
||||
<div class="toggle-switch">
|
||||
<input type="checkbox" id="SHOW_THINKING_PROCESS" name="SHOW_THINKING_PROCESS">
|
||||
<span class="toggle-slider"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图像生成相关配置 -->
|
||||
<div class="config-section" id="image-section">
|
||||
<h2><i class="fas fa-image"></i> 图像生成配置</h2>
|
||||
|
||||
<div class="config-item">
|
||||
<label for="PAID_KEY">付费API密钥</label>
|
||||
<input type="text" id="PAID_KEY" name="PAID_KEY" placeholder="AIzaSyxxxxxxxxxxxxxxxxxxx">
|
||||
<small class="help-text">用于图像生成的付费API密钥</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item">
|
||||
<label for="CREATE_IMAGE_MODEL">图像生成模型</label>
|
||||
<input type="text" id="CREATE_IMAGE_MODEL" name="CREATE_IMAGE_MODEL" placeholder="imagen-3.0-generate-002">
|
||||
<small class="help-text">用于图像生成的模型</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item">
|
||||
<label for="UPLOAD_PROVIDER">上传提供商</label>
|
||||
<select id="UPLOAD_PROVIDER" name="UPLOAD_PROVIDER">
|
||||
<option value="smms" selected>SM.MS</option>
|
||||
<option value="picgo">PicGo</option>
|
||||
<option value="cloudflare">Cloudflare</option>
|
||||
</select>
|
||||
<small class="help-text">图片上传服务提供商</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item provider-config" data-provider="smms">
|
||||
<label for="SMMS_SECRET_TOKEN">SM.MS密钥</label>
|
||||
<input type="text" id="SMMS_SECRET_TOKEN" name="SMMS_SECRET_TOKEN" placeholder="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX">
|
||||
<small class="help-text">SM.MS图床的密钥</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item provider-config" data-provider="picgo">
|
||||
<label for="PICGO_API_KEY">PicGo API密钥</label>
|
||||
<input type="text" id="PICGO_API_KEY" name="PICGO_API_KEY" placeholder="xxxx">
|
||||
<small class="help-text">PicGo的API密钥</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item provider-config" data-provider="cloudflare">
|
||||
<label for="CLOUDFLARE_IMGBED_URL">Cloudflare图床URL</label>
|
||||
<input type="text" id="CLOUDFLARE_IMGBED_URL" name="CLOUDFLARE_IMGBED_URL" placeholder="https://xxxxxxx.pages.dev/upload">
|
||||
<small class="help-text">Cloudflare图床的URL</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item provider-config" data-provider="cloudflare">
|
||||
<label for="CLOUDFLARE_IMGBED_AUTH_CODE">Cloudflare认证码</label>
|
||||
<input type="text" id="CLOUDFLARE_IMGBED_AUTH_CODE" name="CLOUDFLARE_IMGBED_AUTH_CODE" placeholder="xxxxxxxxx">
|
||||
<small class="help-text">Cloudflare图床的认证码</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 流式输出优化器配置 -->
|
||||
<div class="config-section" id="stream-section">
|
||||
<h2><i class="fas fa-stream"></i> 流式输出优化器</h2>
|
||||
|
||||
<div class="config-item toggle">
|
||||
<label for="STREAM_OPTIMIZER_ENABLED">启用流式输出优化</label>
|
||||
<div class="toggle-switch">
|
||||
<input type="checkbox" id="STREAM_OPTIMIZER_ENABLED" name="STREAM_OPTIMIZER_ENABLED">
|
||||
<span class="toggle-slider"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-item">
|
||||
<label for="STREAM_MIN_DELAY">最小延迟(秒)</label>
|
||||
<input type="number" id="STREAM_MIN_DELAY" name="STREAM_MIN_DELAY" min="0" max="1" step="0.001">
|
||||
<small class="help-text">流式输出的最小延迟时间</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item">
|
||||
<label for="STREAM_MAX_DELAY">最大延迟(秒)</label>
|
||||
<input type="number" id="STREAM_MAX_DELAY" name="STREAM_MAX_DELAY" min="0" max="1" step="0.001">
|
||||
<small class="help-text">流式输出的最大延迟时间</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item">
|
||||
<label for="STREAM_SHORT_TEXT_THRESHOLD">短文本阈值</label>
|
||||
<input type="number" id="STREAM_SHORT_TEXT_THRESHOLD" name="STREAM_SHORT_TEXT_THRESHOLD" min="1" max="100">
|
||||
<small class="help-text">短文本的字符阈值</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item">
|
||||
<label for="STREAM_LONG_TEXT_THRESHOLD">长文本阈值</label>
|
||||
<input type="number" id="STREAM_LONG_TEXT_THRESHOLD" name="STREAM_LONG_TEXT_THRESHOLD" min="1" max="1000">
|
||||
<small class="help-text">长文本的字符阈值</small>
|
||||
</div>
|
||||
|
||||
<div class="config-item">
|
||||
<label for="STREAM_CHUNK_SIZE">分块大小</label>
|
||||
<input type="number" id="STREAM_CHUNK_SIZE" name="STREAM_CHUNK_SIZE" min="1" max="100">
|
||||
<small class="help-text">流式输出的分块大小</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" id="saveBtn" class="save-btn">
|
||||
<i class="fas fa-save"></i> 保存配置
|
||||
</button>
|
||||
<button type="button" id="resetBtn" class="reset-btn">
|
||||
<i class="fas fa-undo"></i> 重置配置
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="scroll-buttons">
|
||||
<button class="scroll-btn" onclick="scrollToTop()" title="回到顶部">
|
||||
<i class="fas fa-chevron-up"></i>
|
||||
</button>
|
||||
<button class="scroll-btn" onclick="scrollToBottom()" title="滚动到底部">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="notification" class="notification"></div>
|
||||
|
||||
<div class="copyright">
|
||||
© <script>document.write(new Date().getFullYear())</script> by <a href="https://linux.do/u/snaily" target="_blank"><img src="https://linux.do/user_avatar/linux.do/snaily/288/306510_2.gif" alt="snaily">snaily</a> |
|
||||
<a href="https://github.com/snailyp/gemini-balance" target="_blank"><i class="fab fa-github"></i> GitHub</a>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/config_editor.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -19,7 +19,15 @@
|
||||
<button class="refresh-btn" onclick="refreshPage(this)">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
<h1>API密钥状态</h1>
|
||||
<h1>Gemini Balance</h1>
|
||||
<div class="nav-tabs">
|
||||
<a href="/config" class="tab-link">
|
||||
<i class="fas fa-cog"></i> 配置编辑
|
||||
</a>
|
||||
<a href="/keys" class="tab-link active">
|
||||
<i class="fas fa-key"></i> 密钥管理
|
||||
</a>
|
||||
</div>
|
||||
<div class="key-list">
|
||||
<h2 onclick="toggleSection(this, 'validKeys')">
|
||||
<span>
|
||||
|
||||
Reference in New Issue
Block a user