Upload 76 files
Browse files- app/api/client_disconnect.py +1 -1
- app/api/gemini_handlers.py +9 -7
- app/api/nonstream_handlers.py +1 -1
- app/api/request_handlers.py +12 -23
- app/api/routes.py +4 -11
- app/api/stream_handlers.py +3 -3
- app/config/settings.py +4 -0
- app/services/gemini.py +71 -65
- app/templates/assets/index.css +1 -1
- app/templates/assets/index.html +3 -3
- app/templates/assets/main.js +0 -0
- app/utils/api_key.py +8 -7
- app/utils/error_handling.py +21 -38
- hajimiUI/src/assets/base.css +51 -1
- hajimiUI/src/components/dashboard/ConfigSection.vue +22 -10
- hajimiUI/src/components/dashboard/LogSection.vue +26 -17
- hajimiUI/src/components/dashboard/StatusSection.vue +39 -28
- hajimiUI/src/stores/dashboard.js +30 -2
- hajimiUI/src/views/DashboardView.vue +135 -11
- hajimiUI/vite.config.js +1 -1
- readme.md +5 -0
- version.txt +1 -1
app/api/client_disconnect.py
CHANGED
|
@@ -3,7 +3,7 @@ import time
|
|
| 3 |
from fastapi import Request
|
| 4 |
from app.models import ChatCompletionRequest
|
| 5 |
from app.utils import create_error_response
|
| 6 |
-
from .
|
| 7 |
|
| 8 |
# 客户端断开检测函数
|
| 9 |
async def check_client_disconnect(http_request: Request, current_api_key: str, request_type: str, model: str):
|
|
|
|
| 3 |
from fastapi import Request
|
| 4 |
from app.models import ChatCompletionRequest
|
| 5 |
from app.utils import create_error_response
|
| 6 |
+
from app.utils.logging import log
|
| 7 |
|
| 8 |
# 客户端断开检测函数
|
| 9 |
async def check_client_disconnect(http_request: Request, current_api_key: str, request_type: str, model: str):
|
app/api/gemini_handlers.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import asyncio
|
| 2 |
from app.models import ChatCompletionRequest
|
| 3 |
from app.services import GeminiClient
|
| 4 |
-
from .
|
| 5 |
|
| 6 |
# Gemini完成请求函数
|
| 7 |
async def run_gemini_completion(
|
|
@@ -35,7 +35,8 @@ async def run_gemini_completion(
|
|
| 35 |
|
| 36 |
# 只在第一次调用时记录完成日志
|
| 37 |
if not hasattr(run_fn, 'logged_complete'):
|
| 38 |
-
log('info', "非流式请求成功完成",
|
|
|
|
| 39 |
run_fn.logged_complete = True
|
| 40 |
return response_content
|
| 41 |
except asyncio.CancelledError:
|
|
@@ -44,14 +45,15 @@ async def run_gemini_completion(
|
|
| 44 |
try:
|
| 45 |
# 使用shield确保任务不被取消,并等待它完成
|
| 46 |
response_content = await asyncio.shield(response_future)
|
| 47 |
-
log('info', "API请求在客户端断开后完成",
|
|
|
|
| 48 |
return response_content
|
| 49 |
except Exception as e:
|
| 50 |
-
|
| 51 |
-
|
| 52 |
raise
|
| 53 |
|
| 54 |
# 如果任务尚未开始或已经失败,记录日志
|
| 55 |
-
|
| 56 |
-
|
| 57 |
raise
|
|
|
|
| 1 |
import asyncio
|
| 2 |
from app.models import ChatCompletionRequest
|
| 3 |
from app.services import GeminiClient
|
| 4 |
+
from app.utils.logging import log
|
| 5 |
|
| 6 |
# Gemini完成请求函数
|
| 7 |
async def run_gemini_completion(
|
|
|
|
| 35 |
|
| 36 |
# 只在第一次调用时记录完成日志
|
| 37 |
if not hasattr(run_fn, 'logged_complete'):
|
| 38 |
+
log('info', "非流式请求成功完成",
|
| 39 |
+
extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
|
| 40 |
run_fn.logged_complete = True
|
| 41 |
return response_content
|
| 42 |
except asyncio.CancelledError:
|
|
|
|
| 45 |
try:
|
| 46 |
# 使用shield确保任务不被取消,并等待它完成
|
| 47 |
response_content = await asyncio.shield(response_future)
|
| 48 |
+
log('info', "API请求在客户端断开后完成",
|
| 49 |
+
extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
|
| 50 |
return response_content
|
| 51 |
except Exception as e:
|
| 52 |
+
log('info', "API调用因客户端断开而失败",
|
| 53 |
+
extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model, 'error_message': f'API请求在客户端断开后失败: {str(e)}'})
|
| 54 |
raise
|
| 55 |
|
| 56 |
# 如果任务尚未开始或已经失败,记录日志
|
| 57 |
+
log('info', "API调用因客户端断开而取消",
|
| 58 |
+
extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model, 'error_message': '客户端断开导致API调用取消'})
|
| 59 |
raise
|
app/api/nonstream_handlers.py
CHANGED
|
@@ -3,7 +3,7 @@ from fastapi import HTTPException, status, Request
|
|
| 3 |
from app.models import ChatCompletionRequest
|
| 4 |
from app.services import GeminiClient
|
| 5 |
from app.utils import cache_response, update_api_call_stats
|
| 6 |
-
from .
|
| 7 |
from .client_disconnect import check_client_disconnect, handle_client_disconnect
|
| 8 |
from .gemini_handlers import run_gemini_completion
|
| 9 |
|
|
|
|
| 3 |
from app.models import ChatCompletionRequest
|
| 4 |
from app.services import GeminiClient
|
| 5 |
from app.utils import cache_response, update_api_call_stats
|
| 6 |
+
from app.utils.logging import log
|
| 7 |
from .client_disconnect import check_client_disconnect, handle_client_disconnect
|
| 8 |
from .gemini_handlers import run_gemini_completion
|
| 9 |
|
app/api/request_handlers.py
CHANGED
|
@@ -6,7 +6,7 @@ from fastapi.responses import StreamingResponse
|
|
| 6 |
from app.models import ChatCompletionRequest
|
| 7 |
from app.services import GeminiClient
|
| 8 |
from app.utils import protect_from_abuse, handle_gemini_error, handle_api_error
|
| 9 |
-
from .
|
| 10 |
from .stream_handlers import process_stream_request
|
| 11 |
from .nonstream_handlers import process_nonstream_request
|
| 12 |
|
|
@@ -35,31 +35,29 @@ async def process_request(
|
|
| 35 |
protect_from_abuse(
|
| 36 |
http_request, MAX_REQUESTS_PER_MINUTE, MAX_REQUESTS_PER_DAY_PER_IP)
|
| 37 |
if chat_request.model not in GeminiClient.AVAILABLE_MODELS:
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
log('error', error_msg, extra=extra_log)
|
| 41 |
raise HTTPException(
|
| 42 |
-
status_code=status.HTTP_400_BAD_REQUEST, detail=
|
| 43 |
|
| 44 |
# 重置已尝试的密钥
|
| 45 |
key_manager.reset_tried_keys_for_request()
|
| 46 |
-
|
| 47 |
# 转换消息格式
|
| 48 |
contents, system_instruction = GeminiClient.convert_messages(
|
| 49 |
-
GeminiClient, chat_request.messages)
|
| 50 |
|
| 51 |
# 设置重试次数(使用可用API密钥数量作为最大重试次数)
|
| 52 |
retry_attempts = len(key_manager.api_keys) if key_manager.api_keys else 1
|
| 53 |
|
| 54 |
# 尝试使用不同API密钥
|
| 55 |
for attempt in range(1, retry_attempts + 1):
|
| 56 |
-
# 获取
|
| 57 |
current_api_key = key_manager.get_available_key()
|
| 58 |
|
| 59 |
# 检查API密钥是否可用
|
| 60 |
if current_api_key is None:
|
| 61 |
log('warning', "没有可用的 API 密钥,跳过本次尝试",
|
| 62 |
-
extra={'request_type': request_type, 'model': chat_request.model
|
| 63 |
break
|
| 64 |
|
| 65 |
# 记录当前尝试的密钥信息
|
|
@@ -87,12 +85,11 @@ async def process_request(
|
|
| 87 |
FAKE_STREAMING_INTERVAL
|
| 88 |
)
|
| 89 |
except Exception as e:
|
| 90 |
-
# 捕获流式请求的异常,但不立即返回错误
|
| 91 |
# 记录错误并继续尝试下一个API密钥
|
| 92 |
error_detail = handle_gemini_error(e, current_api_key, key_manager)
|
| 93 |
-
log('
|
| 94 |
extra={'key': current_api_key[:8], 'request_type': 'stream', 'model': chat_request.model})
|
| 95 |
-
# 不返回错误,而是抛出异常让外层循环处理
|
| 96 |
raise
|
| 97 |
else:
|
| 98 |
return await process_nonstream_request(
|
|
@@ -112,7 +109,7 @@ async def process_request(
|
|
| 112 |
)
|
| 113 |
except HTTPException as e:
|
| 114 |
if e.status_code == status.HTTP_408_REQUEST_TIMEOUT:
|
| 115 |
-
log('
|
| 116 |
extra={'key': current_api_key[:8], 'request_type': request_type,
|
| 117 |
'model': chat_request.model, 'status_code': 408})
|
| 118 |
raise
|
|
@@ -135,21 +132,13 @@ async def process_request(
|
|
| 135 |
extra={'cache_operation': 'remove-on-error', 'request_type': request_type})
|
| 136 |
del response_cache_manager.cache[cache_key]
|
| 137 |
|
| 138 |
-
if error_result.get('should_retry', False):
|
| 139 |
-
# 服务器错误需要重试(等待已在handle_api_error中完成)
|
| 140 |
-
continue
|
| 141 |
-
elif error_result.get('should_switch_key', False) and attempt < retry_attempts:
|
| 142 |
-
# 跳出服务器错误重试循环,获取下一个可用密钥
|
| 143 |
-
log('info', f"API密钥 {current_api_key[:8]}... 失败,准备尝试下一个密钥",
|
| 144 |
-
extra={'key': current_api_key[:8], 'request_type': request_type})
|
| 145 |
-
break
|
| 146 |
else:
|
| 147 |
-
#
|
| 148 |
break
|
| 149 |
|
| 150 |
# 如果所有尝试都失败
|
| 151 |
msg = "所有API密钥均请求失败,请稍后重试"
|
| 152 |
-
log('error', "API key 替换失败,所有API key都已尝试,请重新配置或稍后重试", extra={'
|
| 153 |
|
| 154 |
# 对于流式请求,创建一个特殊的StreamingResponse返回错误
|
| 155 |
if chat_request.stream:
|
|
|
|
| 6 |
from app.models import ChatCompletionRequest
|
| 7 |
from app.services import GeminiClient
|
| 8 |
from app.utils import protect_from_abuse, handle_gemini_error, handle_api_error
|
| 9 |
+
from app.utils.logging import log
|
| 10 |
from .stream_handlers import process_stream_request
|
| 11 |
from .nonstream_handlers import process_nonstream_request
|
| 12 |
|
|
|
|
| 35 |
protect_from_abuse(
|
| 36 |
http_request, MAX_REQUESTS_PER_MINUTE, MAX_REQUESTS_PER_DAY_PER_IP)
|
| 37 |
if chat_request.model not in GeminiClient.AVAILABLE_MODELS:
|
| 38 |
+
log('error', "无效的模型",
|
| 39 |
+
extra={'request_type': request_type, 'model': chat_request.model, 'status_code': 400})
|
|
|
|
| 40 |
raise HTTPException(
|
| 41 |
+
status_code=status.HTTP_400_BAD_REQUEST, detail="无效的模型")
|
| 42 |
|
| 43 |
# 重置已尝试的密钥
|
| 44 |
key_manager.reset_tried_keys_for_request()
|
|
|
|
| 45 |
# 转换消息格式
|
| 46 |
contents, system_instruction = GeminiClient.convert_messages(
|
| 47 |
+
GeminiClient, chat_request.messages,model=chat_request.model)
|
| 48 |
|
| 49 |
# 设置重试次数(使用可用API密钥数量作为最大重试次数)
|
| 50 |
retry_attempts = len(key_manager.api_keys) if key_manager.api_keys else 1
|
| 51 |
|
| 52 |
# 尝试使用不同API密钥
|
| 53 |
for attempt in range(1, retry_attempts + 1):
|
| 54 |
+
# 获取密钥
|
| 55 |
current_api_key = key_manager.get_available_key()
|
| 56 |
|
| 57 |
# 检查API密钥是否可用
|
| 58 |
if current_api_key is None:
|
| 59 |
log('warning', "没有可用的 API 密钥,跳过本次尝试",
|
| 60 |
+
extra={'request_type': request_type, 'model': chat_request.model})
|
| 61 |
break
|
| 62 |
|
| 63 |
# 记录当前尝试的密钥信息
|
|
|
|
| 85 |
FAKE_STREAMING_INTERVAL
|
| 86 |
)
|
| 87 |
except Exception as e:
|
| 88 |
+
# 捕获流式请求的异常,但不立即返回错误,而是抛出异常让外层循环处理
|
| 89 |
# 记录错误并继续尝试下一个API密钥
|
| 90 |
error_detail = handle_gemini_error(e, current_api_key, key_manager)
|
| 91 |
+
log('info', f"流式请求失败: {error_detail}",
|
| 92 |
extra={'key': current_api_key[:8], 'request_type': 'stream', 'model': chat_request.model})
|
|
|
|
| 93 |
raise
|
| 94 |
else:
|
| 95 |
return await process_nonstream_request(
|
|
|
|
| 109 |
)
|
| 110 |
except HTTPException as e:
|
| 111 |
if e.status_code == status.HTTP_408_REQUEST_TIMEOUT:
|
| 112 |
+
log('info', "客户端连接中断",
|
| 113 |
extra={'key': current_api_key[:8], 'request_type': request_type,
|
| 114 |
'model': chat_request.model, 'status_code': 408})
|
| 115 |
raise
|
|
|
|
| 132 |
extra={'cache_operation': 'remove-on-error', 'request_type': request_type})
|
| 133 |
del response_cache_manager.cache[cache_key]
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
else:
|
| 136 |
+
# 跳出循环
|
| 137 |
break
|
| 138 |
|
| 139 |
# 如果所有尝试都失败
|
| 140 |
msg = "所有API密钥均请求失败,请稍后重试"
|
| 141 |
+
log('error', "API key 替换失败,所有API key都已尝试,请重新配置或稍后重试", extra={'request_type': 'switch_key'})
|
| 142 |
|
| 143 |
# 对于流式请求,创建一个特殊的StreamingResponse返回错误
|
| 144 |
if chat_request.stream:
|
app/api/routes.py
CHANGED
|
@@ -2,23 +2,18 @@ from fastapi import APIRouter, HTTPException, Request, Depends, status
|
|
| 2 |
from fastapi.responses import JSONResponse, StreamingResponse
|
| 3 |
from app.models import ChatCompletionRequest, ChatCompletionResponse, ErrorResponse, ModelList
|
| 4 |
from app.services import GeminiClient
|
| 5 |
-
from app.utils import
|
| 6 |
-
|
| 7 |
-
cache_response,
|
| 8 |
-
create_chat_response,
|
| 9 |
-
create_error_response
|
| 10 |
-
)
|
| 11 |
from app.config.settings import (
|
| 12 |
api_call_stats,
|
| 13 |
BLOCKED_MODELS
|
| 14 |
)
|
| 15 |
import asyncio
|
| 16 |
import time
|
| 17 |
-
|
| 18 |
|
| 19 |
# 导入拆分后的模块
|
| 20 |
from .auth import verify_password
|
| 21 |
-
from .logging_utils import log
|
| 22 |
from .request_handlers import process_request
|
| 23 |
|
| 24 |
# 创建路由器
|
|
@@ -83,7 +78,6 @@ def list_models():
|
|
| 83 |
async def chat_completions(request: ChatCompletionRequest, http_request: Request, _: None = Depends(custom_verify_password)):
|
| 84 |
# 获取客户端IP
|
| 85 |
client_ip = http_request.client.host if http_request.client else "unknown"
|
| 86 |
-
|
| 87 |
# 流式请求直接处理,不使用缓存
|
| 88 |
if request.stream:
|
| 89 |
return await process_request(
|
|
@@ -101,7 +95,6 @@ async def chat_completions(request: ChatCompletionRequest, http_request: Request
|
|
| 101 |
MAX_REQUESTS_PER_MINUTE,
|
| 102 |
MAX_REQUESTS_PER_DAY_PER_IP
|
| 103 |
)
|
| 104 |
-
|
| 105 |
# 生成完整缓存键 - 用于精确匹配
|
| 106 |
cache_key = generate_cache_key(request)
|
| 107 |
|
|
@@ -181,7 +174,7 @@ async def chat_completions(request: ChatCompletionRequest, http_request: Request
|
|
| 181 |
log('info', f"已从活跃请求池移除{error_type}任务: {pool_key}",
|
| 182 |
extra={'request_type': 'non-stream'})
|
| 183 |
|
| 184 |
-
# 创建请求处理任务
|
| 185 |
process_task = asyncio.create_task(
|
| 186 |
process_request(
|
| 187 |
request,
|
|
|
|
| 2 |
from fastapi.responses import JSONResponse, StreamingResponse
|
| 3 |
from app.models import ChatCompletionRequest, ChatCompletionResponse, ErrorResponse, ModelList
|
| 4 |
from app.services import GeminiClient
|
| 5 |
+
from app.utils import generate_cache_key
|
| 6 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
from app.config.settings import (
|
| 8 |
api_call_stats,
|
| 9 |
BLOCKED_MODELS
|
| 10 |
)
|
| 11 |
import asyncio
|
| 12 |
import time
|
| 13 |
+
from app.utils.logging import log
|
| 14 |
|
| 15 |
# 导入拆分后的模块
|
| 16 |
from .auth import verify_password
|
|
|
|
| 17 |
from .request_handlers import process_request
|
| 18 |
|
| 19 |
# 创建路由器
|
|
|
|
| 78 |
async def chat_completions(request: ChatCompletionRequest, http_request: Request, _: None = Depends(custom_verify_password)):
|
| 79 |
# 获取客户端IP
|
| 80 |
client_ip = http_request.client.host if http_request.client else "unknown"
|
|
|
|
| 81 |
# 流式请求直接处理,不使用缓存
|
| 82 |
if request.stream:
|
| 83 |
return await process_request(
|
|
|
|
| 95 |
MAX_REQUESTS_PER_MINUTE,
|
| 96 |
MAX_REQUESTS_PER_DAY_PER_IP
|
| 97 |
)
|
|
|
|
| 98 |
# 生成完整缓存键 - 用于精确匹配
|
| 99 |
cache_key = generate_cache_key(request)
|
| 100 |
|
|
|
|
| 174 |
log('info', f"已从活跃请求池移除{error_type}任务: {pool_key}",
|
| 175 |
extra={'request_type': 'non-stream'})
|
| 176 |
|
| 177 |
+
# 创建非流式请求处理任务
|
| 178 |
process_task = asyncio.create_task(
|
| 179 |
process_request(
|
| 180 |
request,
|
app/api/stream_handlers.py
CHANGED
|
@@ -7,7 +7,7 @@ from fastapi.responses import StreamingResponse
|
|
| 7 |
from app.models import ChatCompletionRequest
|
| 8 |
from app.services import GeminiClient
|
| 9 |
from app.utils import handle_gemini_error, update_api_call_stats
|
| 10 |
-
from .
|
| 11 |
|
| 12 |
# 流式请求处理函数
|
| 13 |
async def process_stream_request(
|
|
@@ -262,8 +262,8 @@ async def process_stream_request(
|
|
| 262 |
yield "data: [DONE]\n\n"
|
| 263 |
|
| 264 |
except asyncio.CancelledError:
|
| 265 |
-
|
| 266 |
-
|
| 267 |
except Exception as e:
|
| 268 |
error_detail = handle_gemini_error(e, current_api_key, key_manager)
|
| 269 |
log('error', f"流式请求失败: {error_detail}",
|
|
|
|
| 7 |
from app.models import ChatCompletionRequest
|
| 8 |
from app.services import GeminiClient
|
| 9 |
from app.utils import handle_gemini_error, update_api_call_stats
|
| 10 |
+
from app.utils.logging import log
|
| 11 |
|
| 12 |
# 流式请求处理函数
|
| 13 |
async def process_stream_request(
|
|
|
|
| 262 |
yield "data: [DONE]\n\n"
|
| 263 |
|
| 264 |
except asyncio.CancelledError:
|
| 265 |
+
log('info', "客户端连接已中断",
|
| 266 |
+
extra={'key': current_api_key[:8], 'request_type': 'stream', 'model': chat_request.model})
|
| 267 |
except Exception as e:
|
| 268 |
error_detail = handle_gemini_error(e, current_api_key, key_manager)
|
| 269 |
log('error', f"流式请求失败: {error_detail}",
|
app/config/settings.py
CHANGED
|
@@ -40,6 +40,10 @@ REMOVE_CACHE_AFTER_USE = os.environ.get("REMOVE_CACHE_AFTER_USE", "true").lower(
|
|
| 40 |
REQUEST_HISTORY_EXPIRY_TIME = int(os.environ.get("REQUEST_HISTORY_EXPIRY_TIME", "600")) # 默认10分钟
|
| 41 |
ENABLE_RECONNECT_DETECTION = os.environ.get("ENABLE_RECONNECT_DETECTION", "true").lower() in ["true", "1", "yes"]
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
version={
|
| 45 |
"local_version":"0.0.0",
|
|
|
|
| 40 |
REQUEST_HISTORY_EXPIRY_TIME = int(os.environ.get("REQUEST_HISTORY_EXPIRY_TIME", "600")) # 默认10分钟
|
| 41 |
ENABLE_RECONNECT_DETECTION = os.environ.get("ENABLE_RECONNECT_DETECTION", "true").lower() in ["true", "1", "yes"]
|
| 42 |
|
| 43 |
+
serach={
|
| 44 |
+
"search_mode":os.environ.get("SERACH_MODE", "true").lower() in ["true", "1", "yes"],
|
| 45 |
+
"search_prompt":os.environ.get("SERACH_PROMPT", "(使用搜索工具联网搜索,需要在content中结合搜索内容)").strip('"')
|
| 46 |
+
}
|
| 47 |
|
| 48 |
version={
|
| 49 |
"local_version":"0.0.0",
|
app/services/gemini.py
CHANGED
|
@@ -11,10 +11,13 @@ import logging
|
|
| 11 |
import secrets
|
| 12 |
import string
|
| 13 |
from app.utils import format_log_message
|
|
|
|
| 14 |
from app.config.settings import (
|
| 15 |
RANDOM_STRING,
|
| 16 |
-
RANDOM_STRING_LENGTH
|
|
|
|
| 17 |
)
|
|
|
|
| 18 |
|
| 19 |
def generate_secure_random_string(length):
|
| 20 |
all_characters = string.ascii_letters + string.digits
|
|
@@ -56,10 +59,11 @@ class ResponseWrapper:
|
|
| 56 |
|
| 57 |
def _extract_text(self) -> str:
|
| 58 |
try:
|
|
|
|
| 59 |
for part in self._data['candidates'][0]['content']['parts']:
|
| 60 |
if 'thought' not in part:
|
| 61 |
-
|
| 62 |
-
return
|
| 63 |
except (KeyError, IndexError):
|
| 64 |
return ""
|
| 65 |
|
|
@@ -124,66 +128,73 @@ class GeminiClient:
|
|
| 124 |
def __init__(self, api_key: str):
|
| 125 |
self.api_key = api_key
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
async def stream_chat(self, request: ChatCompletionRequest, contents, safety_settings, system_instruction):
|
| 128 |
-
extra_log = {'key': self.api_key[:8], 'request_type': 'stream', 'model': request.model, 'status_code': 'N/A'}
|
| 129 |
-
log_msg = format_log_message('INFO', "流式请求开始", extra=extra_log)
|
| 130 |
-
logger.info(log_msg)
|
| 131 |
|
| 132 |
# 检查是否启用假流式请求
|
| 133 |
if FAKE_STREAMING:
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
try:
|
| 138 |
-
# 这个方法不再直接使用self.api_key,而是由main.py提供API密钥列表和管理
|
| 139 |
-
# 在这里,我们只负责持续发送换行符,直到main.py那边获取到响应
|
| 140 |
|
| 141 |
-
#
|
| 142 |
start_time = time.time()
|
| 143 |
while True:
|
| 144 |
-
# 发送换行符作为保活消息
|
| 145 |
yield "\n"
|
| 146 |
-
# 等待一段时间
|
| 147 |
await asyncio.sleep(FAKE_STREAMING_INTERVAL)
|
| 148 |
|
| 149 |
-
# 如果等待时间过长(超过300秒),
|
| 150 |
if time.time() - start_time > 300:
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
error_msg = "假流式请求等待时间过长,所有API密钥均已尝试"
|
| 155 |
-
extra_log_timeout = {'key': self.api_key[:8], 'request_type': 'fake-stream', 'model': request.model, 'status_code': 'TIMEOUT', 'error_message': error_msg}
|
| 156 |
-
log_msg = format_log_message('ERROR', error_msg, extra=extra_log_timeout)
|
| 157 |
-
logger.error(log_msg)
|
| 158 |
-
raise TimeoutError(error_msg)
|
| 159 |
|
| 160 |
except Exception as e:
|
| 161 |
-
if not isinstance(e, asyncio.CancelledError):
|
| 162 |
-
|
| 163 |
-
extra_log_error = {'key': self.api_key[:8], 'request_type': 'fake-stream', 'model': request.model, 'status_code': 'ERROR', 'error_message': error_msg}
|
| 164 |
-
log_msg = format_log_message('ERROR', error_msg, extra=extra_log_error)
|
| 165 |
-
logger.error(log_msg)
|
| 166 |
raise e
|
| 167 |
finally:
|
| 168 |
-
|
| 169 |
-
logger.info(log_msg)
|
| 170 |
else:
|
| 171 |
-
#
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
headers = {
|
| 175 |
"Content-Type": "application/json",
|
| 176 |
}
|
| 177 |
-
data = {
|
| 178 |
-
"contents": contents,
|
| 179 |
-
"generationConfig": {
|
| 180 |
-
"temperature": request.temperature,
|
| 181 |
-
"maxOutputTokens": request.max_tokens,
|
| 182 |
-
},
|
| 183 |
-
"safetySettings": safety_settings,
|
| 184 |
-
}
|
| 185 |
-
if system_instruction:
|
| 186 |
-
data["system_instruction"] = system_instruction
|
| 187 |
|
| 188 |
async with httpx.AsyncClient() as client:
|
| 189 |
async with client.stream("POST", url, headers=headers, json=data, timeout=600) as response:
|
|
@@ -241,38 +252,26 @@ class GeminiClient:
|
|
| 241 |
logger.info(log_msg)
|
| 242 |
|
| 243 |
def complete_chat(self, request: ChatCompletionRequest, contents, safety_settings, system_instruction):
|
| 244 |
-
extra_log = {'key': self.api_key[:8], 'request_type': 'non-stream', 'model': request.model
|
| 245 |
-
|
| 246 |
-
logger.info(log_msg)
|
| 247 |
|
| 248 |
-
api_version =
|
| 249 |
-
|
|
|
|
| 250 |
headers = {
|
| 251 |
"Content-Type": "application/json",
|
| 252 |
}
|
| 253 |
-
|
| 254 |
-
"contents": contents,
|
| 255 |
-
"generationConfig": {
|
| 256 |
-
"temperature": request.temperature,
|
| 257 |
-
"maxOutputTokens": request.max_tokens,
|
| 258 |
-
},
|
| 259 |
-
"safetySettings": safety_settings,
|
| 260 |
-
}
|
| 261 |
-
if system_instruction:
|
| 262 |
-
data["system_instruction"] = system_instruction
|
| 263 |
-
|
| 264 |
try:
|
| 265 |
response = requests.post(url, headers=headers, json=data)
|
| 266 |
response.raise_for_status()
|
| 267 |
-
|
| 268 |
-
log_msg = format_log_message('INFO', "非流式请求成功完成", extra=extra_log)
|
| 269 |
-
logger.info(log_msg)
|
| 270 |
|
| 271 |
return ResponseWrapper(response.json())
|
| 272 |
except Exception as e:
|
| 273 |
raise
|
| 274 |
|
| 275 |
-
def convert_messages(self, messages, use_system_prompt=False):
|
| 276 |
gemini_history = []
|
| 277 |
errors = []
|
| 278 |
system_instruction_text = ""
|
|
@@ -341,11 +340,13 @@ class GeminiClient:
|
|
| 341 |
if errors:
|
| 342 |
return errors
|
| 343 |
else:
|
|
|
|
|
|
|
|
|
|
| 344 |
if RANDOM_STRING:
|
| 345 |
gemini_history.insert(1,{'role': 'user', 'parts': [{'text': generate_secure_random_string(RANDOM_STRING_LENGTH)}]})
|
| 346 |
gemini_history.insert(len(gemini_history)-1,{'role': 'user', 'parts': [{'text': generate_secure_random_string(RANDOM_STRING_LENGTH)}]})
|
| 347 |
log_msg = format_log_message('INFO', "伪装消息成功")
|
| 348 |
-
logger.info(log_msg)
|
| 349 |
return gemini_history, {"parts": [{"text": system_instruction_text}]}
|
| 350 |
|
| 351 |
@staticmethod
|
|
@@ -356,6 +357,11 @@ class GeminiClient:
|
|
| 356 |
response = await client.get(url)
|
| 357 |
response.raise_for_status()
|
| 358 |
data = response.json()
|
| 359 |
-
models = [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
models.extend(GeminiClient.EXTRA_MODELS)
|
|
|
|
| 361 |
return models
|
|
|
|
| 11 |
import secrets
|
| 12 |
import string
|
| 13 |
from app.utils import format_log_message
|
| 14 |
+
from app.config import settings
|
| 15 |
from app.config.settings import (
|
| 16 |
RANDOM_STRING,
|
| 17 |
+
RANDOM_STRING_LENGTH,
|
| 18 |
+
serach
|
| 19 |
)
|
| 20 |
+
from app.utils.logging import log
|
| 21 |
|
| 22 |
def generate_secure_random_string(length):
|
| 23 |
all_characters = string.ascii_letters + string.digits
|
|
|
|
| 59 |
|
| 60 |
def _extract_text(self) -> str:
|
| 61 |
try:
|
| 62 |
+
text=""
|
| 63 |
for part in self._data['candidates'][0]['content']['parts']:
|
| 64 |
if 'thought' not in part:
|
| 65 |
+
text+=part['text']
|
| 66 |
+
return part['text']
|
| 67 |
except (KeyError, IndexError):
|
| 68 |
return ""
|
| 69 |
|
|
|
|
| 128 |
def __init__(self, api_key: str):
|
| 129 |
self.api_key = api_key
|
| 130 |
|
| 131 |
+
# 将流式和非流式请求的通用部分提取为共享方法
|
| 132 |
+
def _prepare_request_data(self, request, contents, safety_settings, system_instruction):
|
| 133 |
+
api_version = "v1alpha" if "think" in request.model else "v1beta"
|
| 134 |
+
if serach["search_mode"]:
|
| 135 |
+
data = {
|
| 136 |
+
"contents": contents,
|
| 137 |
+
"tools": [{"google_search": {}}],
|
| 138 |
+
"generationConfig": self._get_generation_config(request),
|
| 139 |
+
"safetySettings": safety_settings,
|
| 140 |
+
}
|
| 141 |
+
else:
|
| 142 |
+
data = {
|
| 143 |
+
"contents": contents,
|
| 144 |
+
"generationConfig": self._get_generation_config(request),
|
| 145 |
+
"safetySettings": safety_settings,
|
| 146 |
+
}
|
| 147 |
+
if system_instruction:
|
| 148 |
+
data["system_instruction"] = system_instruction
|
| 149 |
+
return api_version, data
|
| 150 |
+
|
| 151 |
+
def _get_generation_config(self, request):
|
| 152 |
+
config_params = {
|
| 153 |
+
"temperature": request.temperature,
|
| 154 |
+
"maxOutputTokens": request.max_tokens,
|
| 155 |
+
"topP": request.top_p,
|
| 156 |
+
"stopSequences": request.stop if isinstance(request.stop, list) else [request.stop] if request.stop is not None else None,
|
| 157 |
+
"candidateCount": request.n
|
| 158 |
+
}
|
| 159 |
+
return {k: v for k, v in config_params.items() if v is not None}
|
| 160 |
+
|
| 161 |
async def stream_chat(self, request: ChatCompletionRequest, contents, safety_settings, system_instruction):
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
# 检查是否启用假流式请求
|
| 164 |
if FAKE_STREAMING:
|
| 165 |
+
extra_log={'key': self.api_key[:8], 'request_type': 'fake_stream', 'model': request.model}
|
| 166 |
+
log('INFO', "使用假流式请求模式(发送换行符保持连接)", extra=extra_log)
|
|
|
|
| 167 |
try:
|
|
|
|
|
|
|
| 168 |
|
| 169 |
+
# 每隔一段时间发送换行符作为保活消息,直到外部取消此生成器
|
| 170 |
start_time = time.time()
|
| 171 |
while True:
|
|
|
|
| 172 |
yield "\n"
|
|
|
|
| 173 |
await asyncio.sleep(FAKE_STREAMING_INTERVAL)
|
| 174 |
|
| 175 |
+
# 如果等待时间过长(超过300秒),抛出超时异常,让外部处理
|
| 176 |
if time.time() - start_time > 300:
|
| 177 |
+
log('ERROR', f"假流式请求等待时间过长",extra=extra_log)
|
| 178 |
+
|
| 179 |
+
raise TimeoutError("假流式请求等待时间过长")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
except Exception as e:
|
| 182 |
+
if not isinstance(e, asyncio.CancelledError):
|
| 183 |
+
log('ERROR', f"假流式处理期间发生错误: {str(e)}", extra=extra_log)
|
|
|
|
|
|
|
|
|
|
| 184 |
raise e
|
| 185 |
finally:
|
| 186 |
+
log('INFO', "假流式请求结束", extra=extra_log)
|
|
|
|
| 187 |
else:
|
| 188 |
+
# 真流式请求处理逻辑
|
| 189 |
+
extra_log = {'key': self.api_key[:8], 'request_type': 'stream', 'model': request.model}
|
| 190 |
+
log('INFO', "真流式请求开始", extra=extra_log)
|
| 191 |
+
|
| 192 |
+
api_version, data = self._prepare_request_data(request, contents, safety_settings, system_instruction)
|
| 193 |
+
model= request.model.removesuffix("-search")
|
| 194 |
+
url = f"https://generativelanguage.googleapis.com/{api_version}/models/{model}:streamGenerateContent?key={self.api_key}&alt=sse"
|
| 195 |
headers = {
|
| 196 |
"Content-Type": "application/json",
|
| 197 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
async with httpx.AsyncClient() as client:
|
| 200 |
async with client.stream("POST", url, headers=headers, json=data, timeout=600) as response:
|
|
|
|
| 252 |
logger.info(log_msg)
|
| 253 |
|
| 254 |
def complete_chat(self, request: ChatCompletionRequest, contents, safety_settings, system_instruction):
|
| 255 |
+
extra_log = {'key': self.api_key[:8], 'request_type': 'non-stream', 'model': request.model}
|
| 256 |
+
log('info', "非流式请求开始", extra=extra_log)
|
|
|
|
| 257 |
|
| 258 |
+
api_version, data = self._prepare_request_data(request, contents, safety_settings, system_instruction)
|
| 259 |
+
model= request.model.removesuffix("-search")
|
| 260 |
+
url = f"https://generativelanguage.googleapis.com/{api_version}/models/{model}:generateContent?key={self.api_key}"
|
| 261 |
headers = {
|
| 262 |
"Content-Type": "application/json",
|
| 263 |
}
|
| 264 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
try:
|
| 266 |
response = requests.post(url, headers=headers, json=data)
|
| 267 |
response.raise_for_status()
|
| 268 |
+
log('info', "非流式请求成功完成", extra=extra_log)
|
|
|
|
|
|
|
| 269 |
|
| 270 |
return ResponseWrapper(response.json())
|
| 271 |
except Exception as e:
|
| 272 |
raise
|
| 273 |
|
| 274 |
+
def convert_messages(self, messages, use_system_prompt=False,model=None):
|
| 275 |
gemini_history = []
|
| 276 |
errors = []
|
| 277 |
system_instruction_text = ""
|
|
|
|
| 340 |
if errors:
|
| 341 |
return errors
|
| 342 |
else:
|
| 343 |
+
# 只有当search_mode为真且模型名称以-search结尾时,才添加搜索提示
|
| 344 |
+
if settings.serach["search_mode"] and model and model.endswith("-search"):
|
| 345 |
+
gemini_history.insert(len(gemini_history)-2,{'role': 'user', 'parts': [{'text':settings.serach["search_prompt"]}]})
|
| 346 |
if RANDOM_STRING:
|
| 347 |
gemini_history.insert(1,{'role': 'user', 'parts': [{'text': generate_secure_random_string(RANDOM_STRING_LENGTH)}]})
|
| 348 |
gemini_history.insert(len(gemini_history)-1,{'role': 'user', 'parts': [{'text': generate_secure_random_string(RANDOM_STRING_LENGTH)}]})
|
| 349 |
log_msg = format_log_message('INFO', "伪装消息成功")
|
|
|
|
| 350 |
return gemini_history, {"parts": [{"text": system_instruction_text}]}
|
| 351 |
|
| 352 |
@staticmethod
|
|
|
|
| 357 |
response = await client.get(url)
|
| 358 |
response.raise_for_status()
|
| 359 |
data = response.json()
|
| 360 |
+
models = []
|
| 361 |
+
for model in data.get("models", []):
|
| 362 |
+
models.append(model["name"])
|
| 363 |
+
if model["name"].startswith("models/gemini-2"):
|
| 364 |
+
models.append(model["name"] + "-search")
|
| 365 |
models.extend(GeminiClient.EXTRA_MODELS)
|
| 366 |
+
|
| 367 |
return models
|
app/templates/assets/index.css
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
:root{--vt-c-white: #ffffff;--vt-c-white-soft: #f8f8f8;--vt-c-white-mute: #f2f2f2;--vt-c-black: #181818;--vt-c-black-soft: #222222;--vt-c-black-mute: #282828;--vt-c-indigo: #2c3e50;--vt-c-divider-light-1: rgba(60, 60, 60, .29);--vt-c-divider-light-2: rgba(60, 60, 60, .12);--vt-c-divider-dark-1: rgba(84, 84, 84, .65);--vt-c-divider-dark-2: rgba(84, 84, 84, .48);--vt-c-text-light-1: var(--vt-c-indigo);--vt-c-text-light-2: rgba(60, 60, 60, .66);--vt-c-text-dark-1: var(--vt-c-white);--vt-c-text-dark-2: rgba(235, 235, 235, .64)}:root{--color-background: var(--vt-c-white);--color-background-soft: var(--vt-c-white-soft);--color-background-mute: var(--vt-c-white-mute);--color-border: var(--vt-c-divider-light-2);--color-border-hover: var(--vt-c-divider-light-1);--color-heading: var(--vt-c-text-light-1);--color-text: var(--vt-c-text-light-1);--section-gap: 160px}@media (prefers-color-scheme: dark){:root{--color-background: var(--vt-c-black);--color-background-soft: var(--vt-c-black-soft);--color-background-mute: var(--vt-c-black-mute);--color-border: var(--vt-c-divider-dark-2);--color-border-hover: var(--vt-c-divider-dark-1);--color-heading: var(--vt-c-text-dark-1);--color-text: var(--vt-c-text-dark-2)}}*,*:before,*:after{box-sizing:border-box;margin:0;font-weight:400}body{min-height:100vh;color:var(--color-text);background:var(--color-background);transition:color .5s,background-color .5s;line-height:1.6;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:15px;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app{max-width:1280px;margin:0 auto;padding:1rem;font-weight:400}a,.green{text-decoration:none;color:#00bd7e;transition:.4s;padding:3px}@media (hover: hover){a:hover{background-color:#00bd7e33}}body{margin:0;padding:0}.info-box[data-v-866707fc]{background-color:#fff;border:1px solid #dee2e6;border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 2px 4px #0000000d}@media (max-width: 768px){.info-box[data-v-866707fc]{margin-bottom:12px}}@media (max-width: 480px){.info-box[data-v-866707fc]{margin-bottom:8px}}.status[data-v-866707fc]{color:#28a745;font-weight:700;font-size:18px;margin-bottom:20px;text-align:center}.section-title[data-v-866707fc]{color:#495057;border-bottom:1px solid #dee2e6;padding-bottom:10px;margin-bottom:20px}.stats-grid[data-v-866707fc]{display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin-top:15px;margin-bottom:20px}@media (max-width: 768px){.stats-grid[data-v-866707fc]{gap:6px}}.stat-card[data-v-866707fc]{background-color:#e9ecef;padding:15px;border-radius:8px;text-align:center;box-shadow:0 2px 4px #0000000d;transition:transform .2s}.stat-card[data-v-866707fc]:hover{transform:translateY(-2px);box-shadow:0 4px 8px #0000001a}.stat-value[data-v-866707fc]{font-size:24px;font-weight:700;color:#007bff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stat-label[data-v-866707fc]{font-size:14px;color:#6c757d;margin-top:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}@media (max-width: 768px){.stat-card[data-v-866707fc]{padding:8px 5px}.stat-value[data-v-866707fc]{font-size:16px}.stat-label[data-v-866707fc]{font-size:11px;margin-top:3px}}@media (max-width: 480px){.stat-card[data-v-866707fc]{padding:6px 3px}.stat-value[data-v-866707fc]{font-size:14px}.stat-label[data-v-866707fc]{font-size:10px;margin-top:2px}}.api-key-stats-container[data-v-866707fc]{margin-top:20px}.api-key-stats-list[data-v-866707fc]{display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin-top:15px}@media (max-width: 992px){.api-key-stats-list[data-v-866707fc]{grid-template-columns:repeat(2,1fr)}}@media (max-width: 576px){.api-key-stats-list[data-v-866707fc]{grid-template-columns:1fr}}.api-key-item[data-v-866707fc]{background-color:#f8f9fa;border-radius:8px;padding:15px;box-shadow:0 2px 4px #0000000d}.api-key-header[data-v-866707fc]{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}.api-key-name[data-v-866707fc]{font-weight:700;color:#495057;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:50%}.api-key-usage[data-v-866707fc]{display:flex;align-items:center;gap:10px;white-space:nowrap}.api-key-count[data-v-866707fc]{font-weight:700;color:#007bff}@media (max-width: 768px){.api-key-item[data-v-866707fc]{padding:8px}.api-key-header[data-v-866707fc]{margin-bottom:6px}.api-key-name[data-v-866707fc]{font-size:12px}.api-key-usage[data-v-866707fc]{font-size:12px;gap:5px}}@media (max-width: 480px){.api-key-item[data-v-866707fc]{padding:6px}.api-key-name[data-v-866707fc]{font-size:11px;max-width:45%}.api-key-usage[data-v-866707fc]{font-size:11px;gap:3px}}.progress-container[data-v-866707fc]{width:100%;height:10px;background-color:#e9ecef;border-radius:5px;overflow:hidden}.progress-bar[data-v-866707fc]{height:100%;border-radius:5px;transition:width .3s ease}.progress-bar.low[data-v-866707fc]{background-color:#28a745}.progress-bar.medium[data-v-866707fc]{background-color:#ffc107}.progress-bar.high[data-v-866707fc]{background-color:#dc3545}.model-stats-container[data-v-866707fc]{margin-top:10px;border-top:1px dashed #dee2e6;padding-top:10px}.model-stats-header[data-v-866707fc]{display:flex;justify-content:space-between;align-items:center;cursor:pointer;-webkit-user-select:none;user-select:none;margin-bottom:8px;color:#495057;font-size:14px}.model-stats-title[data-v-866707fc]{font-weight:600}.model-stats-toggle[data-v-866707fc]{font-size:12px}.model-stats-list[data-v-866707fc]{display:flex;flex-direction:column;gap:8px}.model-stat-item[data-v-866707fc]{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background-color:#f1f3f5;border-radius:4px;font-size:13px}.model-name[data-v-866707fc]{font-weight:500;color:#495057;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:60%}.model-count[data-v-866707fc]{display:flex;align-items:center;gap:8px;color:#007bff;font-weight:600}.model-usage-text[data-v-866707fc]{color:#6c757d;font-weight:400;font-size:12px}.model-progress-container[data-v-866707fc]{width:60px;height:6px;background-color:#e9ecef;border-radius:3px;overflow:hidden;margin-left:5px}.model-progress-bar[data-v-866707fc]{height:100%;border-radius:3px;transition:width .3s ease}.view-more-models[data-v-866707fc]{text-align:center;color:#007bff;font-size:12px;cursor:pointer;padding:8px;margin-top:5px;border-radius:4px;background-color:#007bff0d;transition:all .2s ease}.view-more-models[data-v-866707fc]:hover{background-color:#007bff1a;transform:translateY(-1px);box-shadow:0 2px 5px #0000000d}.fold-header[data-v-866707fc]{cursor:pointer;-webkit-user-select:none;user-select:none;display:flex;justify-content:space-between;align-items:center;transition:background-color .2s;border-radius:6px;padding:5px 8px}.fold-header[data-v-866707fc]:hover{background-color:#00000008}.fold-icon[data-v-866707fc]{display:inline-flex;align-items:center;justify-content:center;transition:transform .3s ease}.fold-icon.rotated[data-v-866707fc]{transform:rotate(180deg)}.fold-content[data-v-866707fc]{overflow:hidden}.fold-enter-active[data-v-866707fc],.fold-leave-active[data-v-866707fc]{transition:all .3s ease;max-height:1000px;opacity:1;overflow:hidden}.fold-enter-from[data-v-866707fc],.fold-leave-to[data-v-866707fc]{max-height:0;opacity:0;overflow:hidden}.model-stat-item[data-v-866707fc]{transition:transform .2s,box-shadow .2s}.model-stat-item[data-v-866707fc]:hover{transform:translateY(-2px);box-shadow:0 2px 8px #0000000d}@media (max-width: 768px){.model-stats-container[data-v-866707fc]{margin-top:8px;padding-top:8px}.model-stats-header[data-v-866707fc]{font-size:12px;margin-bottom:6px}.model-stat-item[data-v-866707fc]{padding:4px 8px;font-size:11px}.model-progress-container[data-v-866707fc]{width:40px;height:4px}}.info-box[data-v-f2d215af]{background-color:#fff;border:1px solid #dee2e6;border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 2px 4px #0000000d}@media (max-width: 768px){.info-box[data-v-f2d215af]{margin-bottom:12px}}@media (max-width: 480px){.info-box[data-v-f2d215af]{margin-bottom:8px}}.section-title[data-v-f2d215af]{color:#495057;border-bottom:1px solid #dee2e6;padding-bottom:10px;margin-bottom:20px}.stats-grid[data-v-f2d215af]{display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin-top:15px;margin-bottom:20px}@media (max-width: 768px){.stats-grid[data-v-f2d215af]{gap:6px}}.stat-card[data-v-f2d215af]{background-color:#e9ecef;padding:15px;border-radius:8px;text-align:center;box-shadow:0 2px 4px #0000000d;transition:transform .2s}.stat-card[data-v-f2d215af]:hover{transform:translateY(-2px);box-shadow:0 4px 8px #0000001a}.stat-value[data-v-f2d215af]{font-size:24px;font-weight:700;color:#007bff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stat-label[data-v-f2d215af]{font-size:14px;color:#6c757d;margin-top:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}@media (max-width: 768px){.stat-card[data-v-f2d215af]{padding:8px 5px}.stat-value[data-v-f2d215af]{font-size:16px}.stat-label[data-v-f2d215af]{font-size:11px;margin-top:3px}}@media (max-width: 480px){.stat-card[data-v-f2d215af]{padding:6px 3px}.stat-value[data-v-f2d215af]{font-size:14px}.stat-label[data-v-f2d215af]{font-size:10px;margin-top:2px}}.update-needed[data-v-f2d215af]{color:#dc3545}.up-to-date[data-v-f2d215af]{color:#28a745}.info-box[data-v-f5b7a7c0]{background-color:#fff;border:1px solid #dee2e6;border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 2px 4px #0000000d}@media (max-width: 768px){.info-box[data-v-f5b7a7c0]{margin-bottom:12px}}@media (max-width: 480px){.info-box[data-v-f5b7a7c0]{margin-bottom:8px}}.section-title[data-v-f5b7a7c0]{color:#495057;border-bottom:1px solid #dee2e6;padding-bottom:10px;margin-bottom:20px}.log-filter[data-v-f5b7a7c0]{display:flex;justify-content:center;margin-bottom:15px;gap:10px;flex-wrap:wrap}.log-filter button[data-v-f5b7a7c0]{padding:5px 10px;border:1px solid #ddd;border-radius:4px;background-color:#f8f9fa;cursor:pointer;min-width:60px}.log-filter button.active[data-v-f5b7a7c0]{background-color:#007bff;color:#fff;border-color:#007bff}@media (max-width: 768px){.log-filter[data-v-f5b7a7c0]{gap:6px;margin-bottom:12px}.log-filter button[data-v-f5b7a7c0]{padding:4px 8px;font-size:12px;min-width:50px}}@media (max-width: 480px){.log-filter[data-v-f5b7a7c0]{gap:4px;margin-bottom:10px}.log-filter button[data-v-f5b7a7c0]{padding:3px 6px;font-size:11px;min-width:40px}}.log-container[data-v-f5b7a7c0]{background-color:#f5f5f5;border:1px solid #ddd;border-radius:8px;padding:15px;margin-top:20px;max-height:500px;overflow-y:auto;font-family:monospace;font-size:14px;line-height:1.5}.log-entry[data-v-f5b7a7c0]{margin-bottom:8px;padding:8px;border-radius:4px;word-break:break-word}.log-entry.INFO[data-v-f5b7a7c0]{background-color:#e8f4f8;border-left:4px solid #17a2b8}.log-entry.WARNING[data-v-f5b7a7c0]{background-color:#fff3cd;border-left:4px solid #ffc107}.log-entry.ERROR[data-v-f5b7a7c0]{background-color:#f8d7da;border-left:4px solid #dc3545}.log-entry.DEBUG[data-v-f5b7a7c0]{background-color:#d1ecf1;border-left:4px solid #17a2b8}.log-timestamp[data-v-f5b7a7c0]{color:#6c757d;font-size:12px;margin-right:10px}.log-level[data-v-f5b7a7c0]{font-weight:700;margin-right:10px}.log-level.INFO[data-v-f5b7a7c0]{color:#17a2b8}.log-level.WARNING[data-v-f5b7a7c0]{color:#ffc107}.log-level.ERROR[data-v-f5b7a7c0]{color:#dc3545}.log-level.DEBUG[data-v-f5b7a7c0]{color:#17a2b8}.log-message[data-v-f5b7a7c0]{color:#212529}@media (max-width: 768px){.log-container[data-v-f5b7a7c0]{padding:10px;font-size:13px}.log-entry[data-v-f5b7a7c0]{padding:6px;margin-bottom:6px}.log-timestamp[data-v-f5b7a7c0]{font-size:11px;display:block;margin-bottom:3px}}@media (max-width: 480px){.log-container[data-v-f5b7a7c0]{padding:8px;font-size:12px}.log-entry[data-v-f5b7a7c0]{padding:5px;margin-bottom:5px}.log-timestamp[data-v-f5b7a7c0]{font-size:10px}.log-level[data-v-f5b7a7c0]{margin-right:5px}}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;line-height:1.6;background-color:#f8f9fa;margin:0;padding:0}.dashboard{max-width:1200px;margin:0 auto;padding:20px}h1{color:#333;text-align:center;margin:20px 0;font-size:1.8rem}@media (max-width: 768px){.dashboard{padding:10px 8px}h1{margin:12px 0 10px}}@media (max-width: 480px){.dashboard{padding:6px 4px}h1{margin:8px 0 6px}}.refresh-button{display:block;margin:20px auto;padding:10px 20px;background-color:#007bff;color:#fff;border:none;border-radius:4px;font-size:16px;cursor:pointer;transition:background-color .2s}.refresh-button:hover{background-color:#0069d9}@media (max-width: 768px){:deep(.info-box){padding:10px 6px;margin-bottom:10px;border-radius:6px}:deep(.section-title){font-size:1.1rem;margin-bottom:10px;padding-bottom:6px}:deep(.stats-grid){gap:5px;margin-top:10px;margin-bottom:15px}.refresh-button{margin:15px auto;padding:8px 16px;font-size:14px}}@media (max-width: 480px){:deep(.info-box){padding:8px 4px;margin-bottom:6px;border-radius:5px}:deep(.section-title){font-size:1rem;margin-bottom:8px;padding-bottom:4px}:deep(.stats-grid){gap:4px;margin-top:8px;margin-bottom:10px}.refresh-button{margin:10px auto;padding:6px 12px;font-size:13px}}
|
|
|
|
| 1 |
+
:root{--vt-c-white: #ffffff;--vt-c-white-soft: #f8f8f8;--vt-c-white-mute: #f2f2f2;--vt-c-black: #181818;--vt-c-black-soft: #222222;--vt-c-black-mute: #282828;--vt-c-indigo: #2c3e50;--vt-c-divider-light-1: rgba(60, 60, 60, .29);--vt-c-divider-light-2: rgba(60, 60, 60, .12);--vt-c-divider-dark-1: rgba(84, 84, 84, .65);--vt-c-divider-dark-2: rgba(84, 84, 84, .48);--vt-c-text-light-1: var(--vt-c-indigo);--vt-c-text-light-2: rgba(60, 60, 60, .66);--vt-c-text-dark-1: var(--vt-c-white);--vt-c-text-dark-2: rgba(235, 235, 235, .64)}:root{--color-background: var(--vt-c-white);--color-background-soft: var(--vt-c-white-soft);--color-background-mute: var(--vt-c-white-mute);--color-border: var(--vt-c-divider-light-2);--color-border-hover: var(--vt-c-divider-light-1);--color-heading: var(--vt-c-text-light-1);--color-text: var(--vt-c-text-light-1);--section-gap: 160px;--card-background: #ffffff;--card-border: #e0e0e0;--button-primary: #007bff;--button-primary-hover: #0069d9;--button-text: #ffffff;--stats-item-bg: #f8f9fa;--log-entry-bg: #f8f9fa;--log-entry-border: #e9ecef;--toggle-bg: #ccc;--toggle-active: #007bff}.dark-mode{--color-background: var(--vt-c-black);--color-background-soft: var(--vt-c-black-soft);--color-background-mute: var(--vt-c-black-mute);--color-border: var(--vt-c-divider-dark-2);--color-border-hover: var(--vt-c-divider-dark-1);--color-heading: var(--vt-c-text-dark-1);--color-text: var(--vt-c-text-dark-2);--card-background: #2d2d2d;--card-border: #444444;--button-primary: #0056b3;--button-primary-hover: #004494;--button-text: #ffffff;--stats-item-bg: #333333;--log-entry-bg: #333333;--log-entry-border: #444444;--toggle-bg: #555555;--toggle-active: #0056b3}@media (prefers-color-scheme: dark){:root:not(.dark-mode):not(.light-mode){--color-background: var(--vt-c-black);--color-background-soft: var(--vt-c-black-soft);--color-background-mute: var(--vt-c-black-mute);--color-border: var(--vt-c-divider-dark-2);--color-border-hover: var(--vt-c-divider-dark-1);--color-heading: var(--vt-c-text-dark-1);--color-text: var(--vt-c-text-dark-2);--card-background: #2d2d2d;--card-border: #444444;--button-primary: #0056b3;--button-primary-hover: #004494;--button-text: #ffffff;--stats-item-bg: #333333;--log-entry-bg: #333333;--log-entry-border: #444444;--toggle-bg: #555555;--toggle-active: #0056b3}}*,*:before,*:after{box-sizing:border-box;margin:0;font-weight:400}body{min-height:100vh;color:var(--color-text);background:var(--color-background);transition:color .5s,background-color .5s;line-height:1.6;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:15px;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app{max-width:1280px;margin:0 auto;padding:1rem;font-weight:400}a,.green{text-decoration:none;color:#00bd7e;transition:.4s;padding:3px}@media (hover: hover){a:hover{background-color:#00bd7e33}}body{margin:0;padding:0}.info-box[data-v-ec28689b]{background-color:var(--card-background);border:1px solid var(--card-border);border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 2px 4px #0000000d;transition:background-color .3s,border-color .3s,box-shadow .3s}@media (max-width: 768px){.info-box[data-v-ec28689b]{margin-bottom:12px}}@media (max-width: 480px){.info-box[data-v-ec28689b]{margin-bottom:8px}}.status[data-v-ec28689b]{color:#28a745;font-weight:700;font-size:18px;margin-bottom:20px;text-align:center}.section-title[data-v-ec28689b]{color:var(--color-heading);border-bottom:1px solid var(--color-border);padding-bottom:10px;margin-bottom:20px;transition:color .3s,border-color .3s}.stats-grid[data-v-ec28689b]{display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin-top:15px;margin-bottom:20px}@media (max-width: 768px){.stats-grid[data-v-ec28689b]{gap:6px}}.stat-card[data-v-ec28689b]{background-color:var(--stats-item-bg);padding:15px;border-radius:8px;text-align:center;box-shadow:0 2px 4px #0000000d;transition:transform .2s,background-color .3s,box-shadow .3s}.stat-card[data-v-ec28689b]:hover{transform:translateY(-2px);box-shadow:0 4px 8px #0000001a}.stat-value[data-v-ec28689b]{font-size:24px;font-weight:700;color:var(--button-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .3s}.stat-label[data-v-ec28689b]{font-size:14px;color:var(--color-text);margin-top:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .3s}@media (max-width: 768px){.stat-card[data-v-ec28689b]{padding:8px 5px}.stat-value[data-v-ec28689b]{font-size:16px}.stat-label[data-v-ec28689b]{font-size:11px;margin-top:3px}}@media (max-width: 480px){.stat-card[data-v-ec28689b]{padding:6px 3px}.stat-value[data-v-ec28689b]{font-size:14px}.stat-label[data-v-ec28689b]{font-size:10px;margin-top:2px}}.api-key-stats-container[data-v-ec28689b]{margin-top:20px}.api-key-stats-list[data-v-ec28689b]{display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin-top:15px}@media (max-width: 992px){.api-key-stats-list[data-v-ec28689b]{grid-template-columns:repeat(2,1fr)}}@media (max-width: 576px){.api-key-stats-list[data-v-ec28689b]{grid-template-columns:1fr}}.api-key-item[data-v-ec28689b]{background-color:var(--stats-item-bg);border-radius:8px;padding:15px;box-shadow:0 2px 4px #0000000d;transition:background-color .3s,box-shadow .3s}.api-key-header[data-v-ec28689b]{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}.api-key-name[data-v-ec28689b]{font-weight:700;color:var(--color-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:50%;transition:color .3s}.api-key-usage[data-v-ec28689b]{display:flex;align-items:center;gap:10px;white-space:nowrap}.api-key-count[data-v-ec28689b]{font-weight:700;color:var(--button-primary);transition:color .3s}@media (max-width: 768px){.api-key-item[data-v-ec28689b]{padding:8px}.api-key-header[data-v-ec28689b]{margin-bottom:6px}.api-key-name[data-v-ec28689b]{font-size:12px}.api-key-usage[data-v-ec28689b]{font-size:12px;gap:5px}}@media (max-width: 480px){.api-key-item[data-v-ec28689b]{padding:6px}.api-key-name[data-v-ec28689b]{font-size:11px;max-width:45%}.api-key-usage[data-v-ec28689b]{font-size:11px;gap:3px}}.progress-container[data-v-ec28689b]{width:100%;height:10px;background-color:var(--color-background-soft);border-radius:5px;overflow:hidden;transition:background-color .3s}.progress-bar[data-v-ec28689b]{height:100%;border-radius:5px;transition:width .3s ease,background-color .3s}.progress-bar.low[data-v-ec28689b]{background-color:#28a745}.progress-bar.medium[data-v-ec28689b]{background-color:#ffc107}.progress-bar.high[data-v-ec28689b]{background-color:#dc3545}.model-stats-container[data-v-ec28689b]{margin-top:10px;border-top:1px dashed var(--color-border);padding-top:10px;transition:border-color .3s}.model-stats-header[data-v-ec28689b]{display:flex;justify-content:space-between;align-items:center;cursor:pointer;-webkit-user-select:none;user-select:none;margin-bottom:8px;color:var(--color-heading);font-size:14px;transition:color .3s}.model-stats-title[data-v-ec28689b]{font-weight:600}.model-stats-toggle[data-v-ec28689b]{font-size:12px}.model-stats-list[data-v-ec28689b]{display:flex;flex-direction:column;gap:8px}.model-stat-item[data-v-ec28689b]{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background-color:var(--color-background-mute);border-radius:4px;font-size:13px;transition:transform .2s,box-shadow .2s,background-color .3s}.model-name[data-v-ec28689b]{font-weight:500;color:var(--color-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:60%;transition:color .3s}.model-count[data-v-ec28689b]{display:flex;align-items:center;gap:8px;color:var(--button-primary);font-weight:600;transition:color .3s}.model-usage-text[data-v-ec28689b]{color:var(--color-text);font-weight:400;font-size:12px;transition:color .3s}.model-progress-container[data-v-ec28689b]{width:60px;height:6px;background-color:var(--color-background-soft);border-radius:3px;overflow:hidden;margin-left:5px;transition:background-color .3s}.model-progress-bar[data-v-ec28689b]{height:100%;border-radius:3px;transition:width .3s ease,background-color .3s}.view-more-models[data-v-ec28689b]{text-align:center;color:var(--button-primary);font-size:12px;cursor:pointer;padding:8px;margin-top:5px;border-radius:4px;background-color:#007bff0d;transition:all .2s ease,color .3s,background-color .3s}.view-more-models[data-v-ec28689b]:hover{background-color:#007bff1a;transform:translateY(-1px);box-shadow:0 2px 5px #0000000d}.fold-header[data-v-ec28689b]{cursor:pointer;-webkit-user-select:none;user-select:none;display:flex;justify-content:space-between;align-items:center;transition:background-color .2s;border-radius:6px;padding:5px 8px}.fold-header[data-v-ec28689b]:hover{background-color:var(--color-background-mute)}.fold-icon[data-v-ec28689b]{display:inline-flex;align-items:center;justify-content:center;transition:transform .3s ease}.fold-icon.rotated[data-v-ec28689b]{transform:rotate(180deg)}.fold-content[data-v-ec28689b]{overflow:hidden}.fold-enter-active[data-v-ec28689b],.fold-leave-active[data-v-ec28689b]{transition:all .3s ease;max-height:1000px;opacity:1;overflow:hidden}.fold-enter-from[data-v-ec28689b],.fold-leave-to[data-v-ec28689b]{max-height:0;opacity:0;overflow:hidden}.model-stat-item[data-v-ec28689b]:hover{transform:translateY(-2px);box-shadow:0 2px 8px #0000000d}@media (max-width: 768px){.model-stats-container[data-v-ec28689b]{margin-top:8px;padding-top:8px}.model-stats-header[data-v-ec28689b]{font-size:12px;margin-bottom:6px}.model-stat-item[data-v-ec28689b]{padding:4px 8px;font-size:11px}.model-progress-container[data-v-ec28689b]{width:40px;height:4px}}.info-box[data-v-fda98e8f]{background-color:var(--card-background);border:1px solid var(--card-border);border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 2px 4px #0000000d;transition:background-color .3s,border-color .3s,box-shadow .3s}@media (max-width: 768px){.info-box[data-v-fda98e8f]{margin-bottom:12px}}@media (max-width: 480px){.info-box[data-v-fda98e8f]{margin-bottom:8px}}.section-title[data-v-fda98e8f]{color:var(--color-heading);border-bottom:1px solid var(--color-border);padding-bottom:10px;margin-bottom:20px;transition:color .3s,border-color .3s}.stats-grid[data-v-fda98e8f]{display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin-top:15px;margin-bottom:20px}@media (max-width: 768px){.stats-grid[data-v-fda98e8f]{gap:6px}}.stat-card[data-v-fda98e8f]{background-color:var(--stats-item-bg);padding:15px;border-radius:8px;text-align:center;box-shadow:0 2px 4px #0000000d;transition:transform .2s,background-color .3s,box-shadow .3s}.stat-card[data-v-fda98e8f]:hover{transform:translateY(-2px);box-shadow:0 4px 8px #0000001a}.stat-value[data-v-fda98e8f]{font-size:24px;font-weight:700;color:var(--button-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .3s}.stat-label[data-v-fda98e8f]{font-size:14px;color:var(--color-text);margin-top:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .3s}@media (max-width: 768px){.stat-card[data-v-fda98e8f]{padding:8px 5px}.stat-value[data-v-fda98e8f]{font-size:16px}.stat-label[data-v-fda98e8f]{font-size:11px;margin-top:3px}}@media (max-width: 480px){.stat-card[data-v-fda98e8f]{padding:6px 3px}.stat-value[data-v-fda98e8f]{font-size:14px}.stat-label[data-v-fda98e8f]{font-size:10px;margin-top:2px}}.update-needed[data-v-fda98e8f]{color:#dc3545!important}.up-to-date[data-v-fda98e8f]{color:#28a745!important}.info-box[data-v-c3726400]{background-color:var(--card-background);border:1px solid var(--card-border);border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 2px 4px #0000000d;transition:background-color .3s,border-color .3s,box-shadow .3s}@media (max-width: 768px){.info-box[data-v-c3726400]{margin-bottom:12px}}@media (max-width: 480px){.info-box[data-v-c3726400]{margin-bottom:8px}}.section-title[data-v-c3726400]{color:var(--color-heading);border-bottom:1px solid var(--color-border);padding-bottom:10px;margin-bottom:20px;transition:color .3s,border-color .3s}.log-filter[data-v-c3726400]{display:flex;justify-content:center;margin-bottom:15px;gap:10px;flex-wrap:wrap}.log-filter button[data-v-c3726400]{padding:5px 10px;border:1px solid var(--card-border);border-radius:4px;background-color:var(--stats-item-bg);color:var(--color-text);cursor:pointer;min-width:60px;transition:background-color .3s,color .3s,border-color .3s}.log-filter button.active[data-v-c3726400]{background-color:var(--button-primary);color:var(--button-text);border-color:var(--button-primary)}@media (max-width: 768px){.log-filter[data-v-c3726400]{gap:6px;margin-bottom:12px}.log-filter button[data-v-c3726400]{padding:4px 8px;font-size:12px;min-width:50px}}@media (max-width: 480px){.log-filter[data-v-c3726400]{gap:4px;margin-bottom:10px}.log-filter button[data-v-c3726400]{padding:3px 6px;font-size:11px;min-width:40px}}.log-container[data-v-c3726400]{background-color:var(--log-entry-bg);border:1px solid var(--log-entry-border);border-radius:8px;padding:15px;margin-top:20px;max-height:500px;overflow-y:auto;font-family:monospace;font-size:14px;line-height:1.5;transition:background-color .3s,border-color .3s}.log-entry[data-v-c3726400]{margin-bottom:8px;padding:8px;border-radius:4px;word-break:break-word;transition:background-color .3s,border-color .3s}.log-entry.INFO[data-v-c3726400]{background-color:#17a2b81a;border-left:4px solid #17a2b8}.log-entry.WARNING[data-v-c3726400]{background-color:#ffc1071a;border-left:4px solid #ffc107}.log-entry.ERROR[data-v-c3726400]{background-color:#dc35451a;border-left:4px solid #dc3545}.log-entry.DEBUG[data-v-c3726400]{background-color:#17a2b81a;border-left:4px solid #17a2b8}.log-timestamp[data-v-c3726400]{color:var(--color-text);font-size:12px;margin-right:10px;opacity:.8;transition:color .3s}.log-level[data-v-c3726400]{font-weight:700;margin-right:10px}.log-level.INFO[data-v-c3726400]{color:#17a2b8}.log-level.WARNING[data-v-c3726400]{color:#ffc107}.log-level.ERROR[data-v-c3726400]{color:#dc3545}.log-level.DEBUG[data-v-c3726400]{color:#17a2b8}.log-message[data-v-c3726400]{color:var(--color-text);transition:color .3s}@media (max-width: 768px){.log-container[data-v-c3726400]{padding:10px;font-size:13px}.log-entry[data-v-c3726400]{padding:6px;margin-bottom:6px}.log-timestamp[data-v-c3726400]{font-size:11px;display:block;margin-bottom:3px}}@media (max-width: 480px){.log-container[data-v-c3726400]{padding:8px;font-size:12px}.log-entry[data-v-c3726400]{padding:5px;margin-bottom:5px}.log-timestamp[data-v-c3726400]{font-size:10px}.log-level[data-v-c3726400]{margin-right:5px}}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;line-height:1.6;background-color:var(--color-background);color:var(--color-text);margin:0;padding:0;transition:background-color .3s,color .3s}.dashboard{max-width:1200px;margin:0 auto;padding:20px}.header-container{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}h1{color:var(--color-heading);margin:0;font-size:1.8rem}.theme-toggle{display:flex;align-items:center}.toggle-label{margin-left:8px;font-size:1.2rem}.switch{position:relative;display:inline-block;width:60px;height:30px}.switch input{opacity:0;width:0;height:0}.slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:var(--toggle-bg);transition:.4s}.slider:before{position:absolute;content:"";height:22px;width:22px;left:4px;bottom:4px;background-color:#fff;transition:.4s}input:checked+.slider{background-color:var(--toggle-active)}input:focus+.slider{box-shadow:0 0 1px var(--toggle-active)}input:checked+.slider:before{transform:translate(30px)}.slider.round{border-radius:34px}.slider.round:before{border-radius:50%}@media (max-width: 768px){.dashboard{padding:10px 8px}.header-container{flex-direction:row;justify-content:space-between;align-items:center;margin-bottom:15px}h1{font-size:1.4rem;text-align:left;margin-right:10px}}@media (max-width: 480px){.dashboard{padding:6px 4px}h1{font-size:1.2rem}.switch{width:50px;height:26px}.slider:before{height:18px;width:18px}input:checked+.slider:before{transform:translate(24px)}.toggle-label{margin-left:5px;font-size:1rem}}.refresh-button{display:block;margin:20px auto;padding:10px 20px;background-color:var(--button-primary);color:var(--button-text);border:none;border-radius:4px;font-size:16px;cursor:pointer;transition:background-color .2s}.refresh-button:hover{background-color:var(--button-primary-hover)}@media (max-width: 768px){:deep(.info-box){padding:10px 6px;margin-bottom:10px;border-radius:6px;background-color:var(--card-background);border:1px solid var(--card-border)}:deep(.section-title){font-size:1.1rem;margin-bottom:10px;padding-bottom:6px;color:var(--color-heading);border-bottom:1px solid var(--color-border)}:deep(.stats-grid){gap:5px;margin-top:10px;margin-bottom:15px}.refresh-button{margin:15px auto;padding:8px 16px;font-size:14px}}@media (max-width: 480px){:deep(.info-box){padding:8px 4px;margin-bottom:6px;border-radius:5px}:deep(.section-title){font-size:1rem;margin-bottom:8px;padding-bottom:4px}:deep(.stats-grid){gap:4px;margin-top:8px;margin-bottom:10px}.refresh-button{margin:10px auto;padding:6px 12px;font-size:13px}}
|
app/templates/assets/index.html
CHANGED
|
@@ -2,11 +2,11 @@
|
|
| 2 |
<html lang="">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
-
<link rel="icon" href="/
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
<title>HAJIMI</title>
|
| 8 |
-
<script type="module" crossorigin src="/
|
| 9 |
-
<link rel="stylesheet" crossorigin href="/
|
| 10 |
</head>
|
| 11 |
<body>
|
| 12 |
<div id="app"></div>
|
|
|
|
| 2 |
<html lang="">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
+
<link rel="icon" href="/favicon.ico">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
<title>HAJIMI</title>
|
| 8 |
+
<script type="module" crossorigin src="/main.js"></script>
|
| 9 |
+
<link rel="stylesheet" crossorigin href="/index.css">
|
| 10 |
</head>
|
| 11 |
<body>
|
| 12 |
<div id="app"></div>
|
app/templates/assets/main.js
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
app/utils/api_key.py
CHANGED
|
@@ -28,7 +28,7 @@ class APIKeyManager:
|
|
| 28 |
|
| 29 |
|
| 30 |
def get_available_key(self):
|
| 31 |
-
"""从栈顶获取密钥,栈空
|
| 32 |
while self.key_stack:
|
| 33 |
key = self.key_stack.pop()
|
| 34 |
# if key not in self.api_key_blacklist and key not in self.tried_keys_for_request:
|
|
@@ -36,12 +36,8 @@ class APIKeyManager:
|
|
| 36 |
self.tried_keys_for_request.add(key)
|
| 37 |
return key
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
logger.error(log_msg)
|
| 42 |
-
return None
|
| 43 |
-
|
| 44 |
-
self._reset_key_stack() # 重新生成密钥栈
|
| 45 |
|
| 46 |
# 再次尝试从新栈中获取密钥 (迭代一次)
|
| 47 |
while self.key_stack:
|
|
@@ -51,6 +47,11 @@ class APIKeyManager:
|
|
| 51 |
self.tried_keys_for_request.add(key)
|
| 52 |
return key
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
return None
|
| 55 |
|
| 56 |
|
|
|
|
| 28 |
|
| 29 |
|
| 30 |
def get_available_key(self):
|
| 31 |
+
"""从栈顶获取密钥,若栈空则重新生成"""
|
| 32 |
while self.key_stack:
|
| 33 |
key = self.key_stack.pop()
|
| 34 |
# if key not in self.api_key_blacklist and key not in self.tried_keys_for_request:
|
|
|
|
| 36 |
self.tried_keys_for_request.add(key)
|
| 37 |
return key
|
| 38 |
|
| 39 |
+
# 栈空,重新生成密钥栈
|
| 40 |
+
self._reset_key_stack()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
# 再次尝试从新栈中获取密钥 (迭代一次)
|
| 43 |
while self.key_stack:
|
|
|
|
| 47 |
self.tried_keys_for_request.add(key)
|
| 48 |
return key
|
| 49 |
|
| 50 |
+
if not self.api_keys:
|
| 51 |
+
log_msg = format_log_message('ERROR', "没有配置任何 API 密钥!")
|
| 52 |
+
logger.error(log_msg)
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
return None
|
| 56 |
|
| 57 |
|
app/utils/error_handling.py
CHANGED
|
@@ -3,6 +3,7 @@ import logging
|
|
| 3 |
import asyncio
|
| 4 |
from fastapi import HTTPException, status
|
| 5 |
from app.utils.logging import format_log_message
|
|
|
|
| 6 |
|
| 7 |
logger = logging.getLogger("my_logger")
|
| 8 |
|
|
@@ -51,21 +52,6 @@ def handle_gemini_error(error, current_api_key, key_manager) -> str:
|
|
| 51 |
# key_manager.blacklist_key(current_api_key)
|
| 52 |
|
| 53 |
return error_message
|
| 54 |
-
elif status_code == 500:
|
| 55 |
-
error_message = "服务器内部错误"
|
| 56 |
-
extra_log_500 = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
|
| 57 |
-
log_msg = format_log_message('WARNING', f"{current_api_key[:8]} ... {current_api_key[-3:]} → 500 服务器内部错误", extra=extra_log_500)
|
| 58 |
-
logger.warning(log_msg)
|
| 59 |
-
|
| 60 |
-
return "Gemini API 内部错误"
|
| 61 |
-
|
| 62 |
-
elif status_code == 503:
|
| 63 |
-
error_message = "服务不可用"
|
| 64 |
-
extra_log_503 = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
|
| 65 |
-
log_msg = format_log_message('WARNING', f"{current_api_key[:8]} ... {current_api_key[-3:]} → 503 服务不可用", extra=extra_log_503)
|
| 66 |
-
logger.warning(log_msg)
|
| 67 |
-
|
| 68 |
-
return "Gemini API 服务不可用"
|
| 69 |
else:
|
| 70 |
error_message = f"未知错误: {status_code}"
|
| 71 |
extra_log_other = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
|
|
@@ -102,36 +88,33 @@ def translate_error(message: str) -> str:
|
|
| 102 |
return "服务不可用"
|
| 103 |
return message
|
| 104 |
|
| 105 |
-
async def handle_api_error(e, api_key, key_manager, request_type, model, retry_count=0):
|
| 106 |
-
"""统一处理API错误
|
| 107 |
-
error_detail = handle_gemini_error(e, api_key, key_manager)
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
|
|
|
| 117 |
extra={'key': api_key[:8], 'request_type': request_type, 'model': model, 'status_code': int(status_code)})
|
| 118 |
-
|
| 119 |
|
| 120 |
# 等待后返回重试信号
|
| 121 |
await asyncio.sleep(wait_time)
|
| 122 |
-
return {'
|
| 123 |
|
| 124 |
-
# 重试次数用尽,
|
| 125 |
-
|
| 126 |
-
extra={'key': api_key[:8], 'request_type': request_type, 'model': model, 'status_code': int(status_code)})
|
| 127 |
-
logger.error(log_msg)
|
| 128 |
|
| 129 |
-
# 不
|
| 130 |
raise HTTPException(status_code=int(status_code),
|
| 131 |
detail=f"Gemini API 服务器错误({status_code}),请稍后重试")
|
| 132 |
|
| 133 |
-
# 对于其他错误,返回切换密钥的信号
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
logger.error(log_msg)
|
| 137 |
-
return {'should_retry': False, 'should_switch_key': True, 'error': error_detail, 'remove_cache': True}
|
|
|
|
| 3 |
import asyncio
|
| 4 |
from fastapi import HTTPException, status
|
| 5 |
from app.utils.logging import format_log_message
|
| 6 |
+
from app.utils.logging import log
|
| 7 |
|
| 8 |
logger = logging.getLogger("my_logger")
|
| 9 |
|
|
|
|
| 52 |
# key_manager.blacklist_key(current_api_key)
|
| 53 |
|
| 54 |
return error_message
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
else:
|
| 56 |
error_message = f"未知错误: {status_code}"
|
| 57 |
extra_log_other = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
|
|
|
|
| 88 |
return "服务不可用"
|
| 89 |
return message
|
| 90 |
|
| 91 |
+
async def handle_api_error(e: Exception, api_key: str, key_manager, request_type: str, model: str, retry_count: int = 0):
|
| 92 |
+
"""统一处理API错误"""
|
|
|
|
| 93 |
|
| 94 |
+
if isinstance(e, requests.exceptions.HTTPError) :
|
| 95 |
+
status_code = e.response.status_code
|
| 96 |
+
# 对500和503错误实现自动重试机制, 最多重试3次
|
| 97 |
+
if retry_count < 3 and (status_code == 500 or status_code == 503):
|
| 98 |
+
error_message = 'Gemini API 内部错误' if (status_code == 500) else "Gemini API 服务目前不可用"
|
| 99 |
+
|
| 100 |
+
# 等待时间 : MIN_RETRY_DELAY=1, MAX_RETRY_DELAY=16
|
| 101 |
+
wait_time = min(1 * (2 ** retry_count), 16)
|
| 102 |
+
log('warning', f"{error_message},将等待{wait_time}秒后重试 ({retry_count+1}/3)",
|
| 103 |
extra={'key': api_key[:8], 'request_type': request_type, 'model': model, 'status_code': int(status_code)})
|
| 104 |
+
|
| 105 |
|
| 106 |
# 等待后返回重试信号
|
| 107 |
await asyncio.sleep(wait_time)
|
| 108 |
+
return {'remove_cache': False}
|
| 109 |
|
| 110 |
+
# 重试次数用尽,在日志中输出错误状态码
|
| 111 |
+
log('error', f"Gemini 服务器错误({status_code}), 且重试{retry_count}次后仍然失败",
|
| 112 |
+
extra={'key': api_key[:8], 'request_type': request_type, 'model': model, 'status_code': int(status_code)})
|
|
|
|
| 113 |
|
| 114 |
+
# 不再切换密钥,向客户端抛出HTTP异常
|
| 115 |
raise HTTPException(status_code=int(status_code),
|
| 116 |
detail=f"Gemini API 服务器错误({status_code}),请稍后重试")
|
| 117 |
|
| 118 |
+
# 对于其他错误,返回切换密钥的信号,并输出错误信息到日志中
|
| 119 |
+
error_detail = handle_gemini_error(e, api_key, key_manager)
|
| 120 |
+
return {'should_switch_key': True, 'error': error_detail, 'remove_cache': True}
|
|
|
|
|
|
hajimiUI/src/assets/base.css
CHANGED
|
@@ -34,10 +34,48 @@
|
|
| 34 |
--color-text: var(--vt-c-text-light-1);
|
| 35 |
|
| 36 |
--section-gap: 160px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
}
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
@media (prefers-color-scheme: dark) {
|
| 40 |
-
:root {
|
| 41 |
--color-background: var(--vt-c-black);
|
| 42 |
--color-background-soft: var(--vt-c-black-soft);
|
| 43 |
--color-background-mute: var(--vt-c-black-mute);
|
|
@@ -47,6 +85,18 @@
|
|
| 47 |
|
| 48 |
--color-heading: var(--vt-c-text-dark-1);
|
| 49 |
--color-text: var(--vt-c-text-dark-2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
}
|
| 52 |
|
|
|
|
| 34 |
--color-text: var(--vt-c-text-light-1);
|
| 35 |
|
| 36 |
--section-gap: 160px;
|
| 37 |
+
|
| 38 |
+
/* 自定义颜色变量 */
|
| 39 |
+
--card-background: #ffffff;
|
| 40 |
+
--card-border: #e0e0e0;
|
| 41 |
+
--button-primary: #007bff;
|
| 42 |
+
--button-primary-hover: #0069d9;
|
| 43 |
+
--button-text: #ffffff;
|
| 44 |
+
--stats-item-bg: #f8f9fa;
|
| 45 |
+
--log-entry-bg: #f8f9fa;
|
| 46 |
+
--log-entry-border: #e9ecef;
|
| 47 |
+
--toggle-bg: #ccc;
|
| 48 |
+
--toggle-active: #007bff;
|
| 49 |
}
|
| 50 |
|
| 51 |
+
/* 夜间模式变量 */
|
| 52 |
+
.dark-mode {
|
| 53 |
+
--color-background: var(--vt-c-black);
|
| 54 |
+
--color-background-soft: var(--vt-c-black-soft);
|
| 55 |
+
--color-background-mute: var(--vt-c-black-mute);
|
| 56 |
+
|
| 57 |
+
--color-border: var(--vt-c-divider-dark-2);
|
| 58 |
+
--color-border-hover: var(--vt-c-divider-dark-1);
|
| 59 |
+
|
| 60 |
+
--color-heading: var(--vt-c-text-dark-1);
|
| 61 |
+
--color-text: var(--vt-c-text-dark-2);
|
| 62 |
+
|
| 63 |
+
/* 自定义夜间模式颜色 */
|
| 64 |
+
--card-background: #2d2d2d;
|
| 65 |
+
--card-border: #444444;
|
| 66 |
+
--button-primary: #0056b3;
|
| 67 |
+
--button-primary-hover: #004494;
|
| 68 |
+
--button-text: #ffffff;
|
| 69 |
+
--stats-item-bg: #333333;
|
| 70 |
+
--log-entry-bg: #333333;
|
| 71 |
+
--log-entry-border: #444444;
|
| 72 |
+
--toggle-bg: #555555;
|
| 73 |
+
--toggle-active: #0056b3;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* 保留系统偏好设置的支持 */
|
| 77 |
@media (prefers-color-scheme: dark) {
|
| 78 |
+
:root:not(.dark-mode):not(.light-mode) {
|
| 79 |
--color-background: var(--vt-c-black);
|
| 80 |
--color-background-soft: var(--vt-c-black-soft);
|
| 81 |
--color-background-mute: var(--vt-c-black-mute);
|
|
|
|
| 85 |
|
| 86 |
--color-heading: var(--vt-c-text-dark-1);
|
| 87 |
--color-text: var(--vt-c-text-dark-2);
|
| 88 |
+
|
| 89 |
+
/* 自定义夜间模式颜色 */
|
| 90 |
+
--card-background: #2d2d2d;
|
| 91 |
+
--card-border: #444444;
|
| 92 |
+
--button-primary: #0056b3;
|
| 93 |
+
--button-primary-hover: #004494;
|
| 94 |
+
--button-text: #ffffff;
|
| 95 |
+
--stats-item-bg: #333333;
|
| 96 |
+
--log-entry-bg: #333333;
|
| 97 |
+
--log-entry-border: #444444;
|
| 98 |
+
--toggle-bg: #555555;
|
| 99 |
+
--toggle-active: #0056b3;
|
| 100 |
}
|
| 101 |
}
|
| 102 |
|
hajimiUI/src/components/dashboard/ConfigSection.vue
CHANGED
|
@@ -28,6 +28,10 @@ const dashboardStore = useDashboardStore()
|
|
| 28 |
<!-- 功能配置 -->
|
| 29 |
<h3 class="section-title">功能配置</h3>
|
| 30 |
<div class="stats-grid">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
<div class="stat-card">
|
| 32 |
<div class="stat-value">{{ dashboardStore.config.fakeStreaming ? "启用" : "禁用" }}</div>
|
| 33 |
<div class="stat-label">假流式响应</div>
|
|
@@ -40,6 +44,10 @@ const dashboardStore = useDashboardStore()
|
|
| 40 |
<div class="stat-value">{{ dashboardStore.config.randomString ? "启用" : "禁用" }}</div>
|
| 41 |
<div class="stat-label">伪装信息</div>
|
| 42 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
</div>
|
| 44 |
|
| 45 |
<!-- 版本信息 -->
|
|
@@ -68,12 +76,13 @@ const dashboardStore = useDashboardStore()
|
|
| 68 |
|
| 69 |
<style scoped>
|
| 70 |
.info-box {
|
| 71 |
-
background-color:
|
| 72 |
-
border: 1px solid
|
| 73 |
border-radius: 8px;
|
| 74 |
padding: 20px;
|
| 75 |
margin-bottom: 20px;
|
| 76 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
|
|
| 77 |
}
|
| 78 |
|
| 79 |
/* 移动端优化 - 减小外边距 */
|
|
@@ -90,10 +99,11 @@ const dashboardStore = useDashboardStore()
|
|
| 90 |
}
|
| 91 |
|
| 92 |
.section-title {
|
| 93 |
-
color:
|
| 94 |
-
border-bottom: 1px solid
|
| 95 |
padding-bottom: 10px;
|
| 96 |
margin-bottom: 20px;
|
|
|
|
| 97 |
}
|
| 98 |
|
| 99 |
.stats-grid {
|
|
@@ -112,12 +122,12 @@ const dashboardStore = useDashboardStore()
|
|
| 112 |
}
|
| 113 |
|
| 114 |
.stat-card {
|
| 115 |
-
background-color:
|
| 116 |
padding: 15px;
|
| 117 |
border-radius: 8px;
|
| 118 |
text-align: center;
|
| 119 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 120 |
-
transition: transform 0.2s;
|
| 121 |
}
|
| 122 |
|
| 123 |
.stat-card:hover {
|
|
@@ -128,19 +138,21 @@ const dashboardStore = useDashboardStore()
|
|
| 128 |
.stat-value {
|
| 129 |
font-size: 24px;
|
| 130 |
font-weight: bold;
|
| 131 |
-
color:
|
| 132 |
white-space: nowrap;
|
| 133 |
overflow: hidden;
|
| 134 |
text-overflow: ellipsis;
|
|
|
|
| 135 |
}
|
| 136 |
|
| 137 |
.stat-label {
|
| 138 |
font-size: 14px;
|
| 139 |
-
color:
|
| 140 |
margin-top: 5px;
|
| 141 |
white-space: nowrap;
|
| 142 |
overflow: hidden;
|
| 143 |
text-overflow: ellipsis;
|
|
|
|
| 144 |
}
|
| 145 |
|
| 146 |
/* 移动端优化 - 更紧凑的卡片 */
|
|
@@ -177,10 +189,10 @@ const dashboardStore = useDashboardStore()
|
|
| 177 |
|
| 178 |
/* 版本更新状态样式 */
|
| 179 |
.update-needed {
|
| 180 |
-
color: #dc3545; /* 红色 - 需要更新 */
|
| 181 |
}
|
| 182 |
|
| 183 |
.up-to-date {
|
| 184 |
-
color: #28a745; /* 绿色 - 已是最新 */
|
| 185 |
}
|
| 186 |
</style>
|
|
|
|
| 28 |
<!-- 功能配置 -->
|
| 29 |
<h3 class="section-title">功能配置</h3>
|
| 30 |
<div class="stats-grid">
|
| 31 |
+
<div class="stat-card">
|
| 32 |
+
<div class="stat-value">{{ dashboardStore.config.fakeStreaming ? "启用" : "禁用" }}</div>
|
| 33 |
+
<div class="stat-label">联网搜索</div>
|
| 34 |
+
</div>
|
| 35 |
<div class="stat-card">
|
| 36 |
<div class="stat-value">{{ dashboardStore.config.fakeStreaming ? "启用" : "禁用" }}</div>
|
| 37 |
<div class="stat-label">假流式响应</div>
|
|
|
|
| 44 |
<div class="stat-value">{{ dashboardStore.config.randomString ? "启用" : "禁用" }}</div>
|
| 45 |
<div class="stat-label">伪装信息</div>
|
| 46 |
</div>
|
| 47 |
+
<div class="stat-card">
|
| 48 |
+
<div class="stat-value">{{ dashboardStore.config.fakeStreamingInterval }}字符</div>
|
| 49 |
+
<div class="stat-label">伪装信息长度</div>
|
| 50 |
+
</div>
|
| 51 |
</div>
|
| 52 |
|
| 53 |
<!-- 版本信息 -->
|
|
|
|
| 76 |
|
| 77 |
<style scoped>
|
| 78 |
.info-box {
|
| 79 |
+
background-color: var(--card-background);
|
| 80 |
+
border: 1px solid var(--card-border);
|
| 81 |
border-radius: 8px;
|
| 82 |
padding: 20px;
|
| 83 |
margin-bottom: 20px;
|
| 84 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 85 |
+
transition: background-color 0.3s, border-color 0.3s, box-shadow 0.3s;
|
| 86 |
}
|
| 87 |
|
| 88 |
/* 移动端优化 - 减小外边距 */
|
|
|
|
| 99 |
}
|
| 100 |
|
| 101 |
.section-title {
|
| 102 |
+
color: var(--color-heading);
|
| 103 |
+
border-bottom: 1px solid var(--color-border);
|
| 104 |
padding-bottom: 10px;
|
| 105 |
margin-bottom: 20px;
|
| 106 |
+
transition: color 0.3s, border-color 0.3s;
|
| 107 |
}
|
| 108 |
|
| 109 |
.stats-grid {
|
|
|
|
| 122 |
}
|
| 123 |
|
| 124 |
.stat-card {
|
| 125 |
+
background-color: var(--stats-item-bg);
|
| 126 |
padding: 15px;
|
| 127 |
border-radius: 8px;
|
| 128 |
text-align: center;
|
| 129 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 130 |
+
transition: transform 0.2s, background-color 0.3s, box-shadow 0.3s;
|
| 131 |
}
|
| 132 |
|
| 133 |
.stat-card:hover {
|
|
|
|
| 138 |
.stat-value {
|
| 139 |
font-size: 24px;
|
| 140 |
font-weight: bold;
|
| 141 |
+
color: var(--button-primary);
|
| 142 |
white-space: nowrap;
|
| 143 |
overflow: hidden;
|
| 144 |
text-overflow: ellipsis;
|
| 145 |
+
transition: color 0.3s;
|
| 146 |
}
|
| 147 |
|
| 148 |
.stat-label {
|
| 149 |
font-size: 14px;
|
| 150 |
+
color: var(--color-text);
|
| 151 |
margin-top: 5px;
|
| 152 |
white-space: nowrap;
|
| 153 |
overflow: hidden;
|
| 154 |
text-overflow: ellipsis;
|
| 155 |
+
transition: color 0.3s;
|
| 156 |
}
|
| 157 |
|
| 158 |
/* 移动端优化 - 更紧凑的卡片 */
|
|
|
|
| 189 |
|
| 190 |
/* 版本更新状态样式 */
|
| 191 |
.update-needed {
|
| 192 |
+
color: #dc3545 !important; /* 红色 - 需要更新 */
|
| 193 |
}
|
| 194 |
|
| 195 |
.up-to-date {
|
| 196 |
+
color: #28a745 !important; /* 绿色 - 已是最新 */
|
| 197 |
}
|
| 198 |
</style>
|
hajimiUI/src/components/dashboard/LogSection.vue
CHANGED
|
@@ -57,12 +57,13 @@ watch(() => dashboardStore.logs, async () => {
|
|
| 57 |
|
| 58 |
<style scoped>
|
| 59 |
.info-box {
|
| 60 |
-
background-color:
|
| 61 |
-
border: 1px solid
|
| 62 |
border-radius: 8px;
|
| 63 |
padding: 20px;
|
| 64 |
margin-bottom: 20px;
|
| 65 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
|
|
| 66 |
}
|
| 67 |
|
| 68 |
/* 移动端优化 - 减小外边距 */
|
|
@@ -79,10 +80,11 @@ watch(() => dashboardStore.logs, async () => {
|
|
| 79 |
}
|
| 80 |
|
| 81 |
.section-title {
|
| 82 |
-
color:
|
| 83 |
-
border-bottom: 1px solid
|
| 84 |
padding-bottom: 10px;
|
| 85 |
margin-bottom: 20px;
|
|
|
|
| 86 |
}
|
| 87 |
|
| 88 |
.log-filter {
|
|
@@ -95,17 +97,19 @@ watch(() => dashboardStore.logs, async () => {
|
|
| 95 |
|
| 96 |
.log-filter button {
|
| 97 |
padding: 5px 10px;
|
| 98 |
-
border: 1px solid
|
| 99 |
border-radius: 4px;
|
| 100 |
-
background-color:
|
|
|
|
| 101 |
cursor: pointer;
|
| 102 |
min-width: 60px;
|
|
|
|
| 103 |
}
|
| 104 |
|
| 105 |
.log-filter button.active {
|
| 106 |
-
background-color:
|
| 107 |
-
color:
|
| 108 |
-
border-color:
|
| 109 |
}
|
| 110 |
|
| 111 |
/* 移动端优化 */
|
|
@@ -137,8 +141,8 @@ watch(() => dashboardStore.logs, async () => {
|
|
| 137 |
}
|
| 138 |
|
| 139 |
.log-container {
|
| 140 |
-
background-color:
|
| 141 |
-
border: 1px solid
|
| 142 |
border-radius: 8px;
|
| 143 |
padding: 15px;
|
| 144 |
margin-top: 20px;
|
|
@@ -147,6 +151,7 @@ watch(() => dashboardStore.logs, async () => {
|
|
| 147 |
font-family: monospace;
|
| 148 |
font-size: 14px;
|
| 149 |
line-height: 1.5;
|
|
|
|
| 150 |
}
|
| 151 |
|
| 152 |
.log-entry {
|
|
@@ -154,32 +159,35 @@ watch(() => dashboardStore.logs, async () => {
|
|
| 154 |
padding: 8px;
|
| 155 |
border-radius: 4px;
|
| 156 |
word-break: break-word;
|
|
|
|
| 157 |
}
|
| 158 |
|
| 159 |
.log-entry.INFO {
|
| 160 |
-
background-color:
|
| 161 |
border-left: 4px solid #17a2b8;
|
| 162 |
}
|
| 163 |
|
| 164 |
.log-entry.WARNING {
|
| 165 |
-
background-color:
|
| 166 |
border-left: 4px solid #ffc107;
|
| 167 |
}
|
| 168 |
|
| 169 |
.log-entry.ERROR {
|
| 170 |
-
background-color:
|
| 171 |
border-left: 4px solid #dc3545;
|
| 172 |
}
|
| 173 |
|
| 174 |
.log-entry.DEBUG {
|
| 175 |
-
background-color:
|
| 176 |
border-left: 4px solid #17a2b8;
|
| 177 |
}
|
| 178 |
|
| 179 |
.log-timestamp {
|
| 180 |
-
color:
|
| 181 |
font-size: 12px;
|
| 182 |
margin-right: 10px;
|
|
|
|
|
|
|
| 183 |
}
|
| 184 |
|
| 185 |
.log-level {
|
|
@@ -204,7 +212,8 @@ watch(() => dashboardStore.logs, async () => {
|
|
| 204 |
}
|
| 205 |
|
| 206 |
.log-message {
|
| 207 |
-
color:
|
|
|
|
| 208 |
}
|
| 209 |
|
| 210 |
@media (max-width: 768px) {
|
|
|
|
| 57 |
|
| 58 |
<style scoped>
|
| 59 |
.info-box {
|
| 60 |
+
background-color: var(--card-background);
|
| 61 |
+
border: 1px solid var(--card-border);
|
| 62 |
border-radius: 8px;
|
| 63 |
padding: 20px;
|
| 64 |
margin-bottom: 20px;
|
| 65 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 66 |
+
transition: background-color 0.3s, border-color 0.3s, box-shadow 0.3s;
|
| 67 |
}
|
| 68 |
|
| 69 |
/* 移动端优化 - 减小外边距 */
|
|
|
|
| 80 |
}
|
| 81 |
|
| 82 |
.section-title {
|
| 83 |
+
color: var(--color-heading);
|
| 84 |
+
border-bottom: 1px solid var(--color-border);
|
| 85 |
padding-bottom: 10px;
|
| 86 |
margin-bottom: 20px;
|
| 87 |
+
transition: color 0.3s, border-color 0.3s;
|
| 88 |
}
|
| 89 |
|
| 90 |
.log-filter {
|
|
|
|
| 97 |
|
| 98 |
.log-filter button {
|
| 99 |
padding: 5px 10px;
|
| 100 |
+
border: 1px solid var(--card-border);
|
| 101 |
border-radius: 4px;
|
| 102 |
+
background-color: var(--stats-item-bg);
|
| 103 |
+
color: var(--color-text);
|
| 104 |
cursor: pointer;
|
| 105 |
min-width: 60px;
|
| 106 |
+
transition: background-color 0.3s, color 0.3s, border-color 0.3s;
|
| 107 |
}
|
| 108 |
|
| 109 |
.log-filter button.active {
|
| 110 |
+
background-color: var(--button-primary);
|
| 111 |
+
color: var(--button-text);
|
| 112 |
+
border-color: var(--button-primary);
|
| 113 |
}
|
| 114 |
|
| 115 |
/* 移动端优化 */
|
|
|
|
| 141 |
}
|
| 142 |
|
| 143 |
.log-container {
|
| 144 |
+
background-color: var(--log-entry-bg);
|
| 145 |
+
border: 1px solid var(--log-entry-border);
|
| 146 |
border-radius: 8px;
|
| 147 |
padding: 15px;
|
| 148 |
margin-top: 20px;
|
|
|
|
| 151 |
font-family: monospace;
|
| 152 |
font-size: 14px;
|
| 153 |
line-height: 1.5;
|
| 154 |
+
transition: background-color 0.3s, border-color 0.3s;
|
| 155 |
}
|
| 156 |
|
| 157 |
.log-entry {
|
|
|
|
| 159 |
padding: 8px;
|
| 160 |
border-radius: 4px;
|
| 161 |
word-break: break-word;
|
| 162 |
+
transition: background-color 0.3s, border-color 0.3s;
|
| 163 |
}
|
| 164 |
|
| 165 |
.log-entry.INFO {
|
| 166 |
+
background-color: rgba(23, 162, 184, 0.1);
|
| 167 |
border-left: 4px solid #17a2b8;
|
| 168 |
}
|
| 169 |
|
| 170 |
.log-entry.WARNING {
|
| 171 |
+
background-color: rgba(255, 193, 7, 0.1);
|
| 172 |
border-left: 4px solid #ffc107;
|
| 173 |
}
|
| 174 |
|
| 175 |
.log-entry.ERROR {
|
| 176 |
+
background-color: rgba(220, 53, 69, 0.1);
|
| 177 |
border-left: 4px solid #dc3545;
|
| 178 |
}
|
| 179 |
|
| 180 |
.log-entry.DEBUG {
|
| 181 |
+
background-color: rgba(23, 162, 184, 0.1);
|
| 182 |
border-left: 4px solid #17a2b8;
|
| 183 |
}
|
| 184 |
|
| 185 |
.log-timestamp {
|
| 186 |
+
color: var(--color-text);
|
| 187 |
font-size: 12px;
|
| 188 |
margin-right: 10px;
|
| 189 |
+
opacity: 0.8;
|
| 190 |
+
transition: color 0.3s;
|
| 191 |
}
|
| 192 |
|
| 193 |
.log-level {
|
|
|
|
| 212 |
}
|
| 213 |
|
| 214 |
.log-message {
|
| 215 |
+
color: var(--color-text);
|
| 216 |
+
transition: color 0.3s;
|
| 217 |
}
|
| 218 |
|
| 219 |
@media (max-width: 768px) {
|
hajimiUI/src/components/dashboard/StatusSection.vue
CHANGED
|
@@ -159,12 +159,13 @@
|
|
| 159 |
|
| 160 |
<style scoped>
|
| 161 |
.info-box {
|
| 162 |
-
background-color:
|
| 163 |
-
border: 1px solid
|
| 164 |
border-radius: 8px;
|
| 165 |
padding: 20px;
|
| 166 |
margin-bottom: 20px;
|
| 167 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
|
|
| 168 |
}
|
| 169 |
|
| 170 |
/* 移动端优化 - 减小外边距 */
|
|
@@ -189,10 +190,11 @@
|
|
| 189 |
}
|
| 190 |
|
| 191 |
.section-title {
|
| 192 |
-
color:
|
| 193 |
-
border-bottom: 1px solid
|
| 194 |
padding-bottom: 10px;
|
| 195 |
margin-bottom: 20px;
|
|
|
|
| 196 |
}
|
| 197 |
|
| 198 |
.stats-grid {
|
|
@@ -211,12 +213,12 @@
|
|
| 211 |
}
|
| 212 |
|
| 213 |
.stat-card {
|
| 214 |
-
background-color:
|
| 215 |
padding: 15px;
|
| 216 |
border-radius: 8px;
|
| 217 |
text-align: center;
|
| 218 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 219 |
-
transition: transform 0.2s;
|
| 220 |
}
|
| 221 |
|
| 222 |
.stat-card:hover {
|
|
@@ -227,19 +229,21 @@
|
|
| 227 |
.stat-value {
|
| 228 |
font-size: 24px;
|
| 229 |
font-weight: bold;
|
| 230 |
-
color:
|
| 231 |
white-space: nowrap;
|
| 232 |
overflow: hidden;
|
| 233 |
text-overflow: ellipsis;
|
|
|
|
| 234 |
}
|
| 235 |
|
| 236 |
.stat-label {
|
| 237 |
font-size: 14px;
|
| 238 |
-
color:
|
| 239 |
margin-top: 5px;
|
| 240 |
white-space: nowrap;
|
| 241 |
overflow: hidden;
|
| 242 |
text-overflow: ellipsis;
|
|
|
|
| 243 |
}
|
| 244 |
|
| 245 |
/* 移动端优化 - 更紧凑的卡片 */
|
|
@@ -301,10 +305,11 @@
|
|
| 301 |
}
|
| 302 |
|
| 303 |
.api-key-item {
|
| 304 |
-
background-color:
|
| 305 |
border-radius: 8px;
|
| 306 |
padding: 15px;
|
| 307 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
|
|
| 308 |
}
|
| 309 |
|
| 310 |
.api-key-header {
|
|
@@ -316,11 +321,12 @@
|
|
| 316 |
|
| 317 |
.api-key-name {
|
| 318 |
font-weight: bold;
|
| 319 |
-
color:
|
| 320 |
white-space: nowrap;
|
| 321 |
overflow: hidden;
|
| 322 |
text-overflow: ellipsis;
|
| 323 |
max-width: 50%;
|
|
|
|
| 324 |
}
|
| 325 |
|
| 326 |
.api-key-usage {
|
|
@@ -332,7 +338,8 @@
|
|
| 332 |
|
| 333 |
.api-key-count {
|
| 334 |
font-weight: bold;
|
| 335 |
-
color:
|
|
|
|
| 336 |
}
|
| 337 |
|
| 338 |
/* 移动端优化 - 更紧凑的API密钥项 */
|
|
@@ -375,15 +382,16 @@
|
|
| 375 |
.progress-container {
|
| 376 |
width: 100%;
|
| 377 |
height: 10px;
|
| 378 |
-
background-color:
|
| 379 |
border-radius: 5px;
|
| 380 |
overflow: hidden;
|
|
|
|
| 381 |
}
|
| 382 |
|
| 383 |
.progress-bar {
|
| 384 |
height: 100%;
|
| 385 |
border-radius: 5px;
|
| 386 |
-
transition: width 0.3s ease;
|
| 387 |
}
|
| 388 |
|
| 389 |
.progress-bar.low {
|
|
@@ -401,8 +409,9 @@
|
|
| 401 |
/* 模型统计样式 */
|
| 402 |
.model-stats-container {
|
| 403 |
margin-top: 10px;
|
| 404 |
-
border-top: 1px dashed
|
| 405 |
padding-top: 10px;
|
|
|
|
| 406 |
}
|
| 407 |
|
| 408 |
.model-stats-header {
|
|
@@ -412,8 +421,9 @@
|
|
| 412 |
cursor: pointer;
|
| 413 |
user-select: none;
|
| 414 |
margin-bottom: 8px;
|
| 415 |
-
color:
|
| 416 |
font-size: 14px;
|
|
|
|
| 417 |
}
|
| 418 |
|
| 419 |
.model-stats-title {
|
|
@@ -435,59 +445,64 @@
|
|
| 435 |
justify-content: space-between;
|
| 436 |
align-items: center;
|
| 437 |
padding: 6px 10px;
|
| 438 |
-
background-color:
|
| 439 |
border-radius: 4px;
|
| 440 |
font-size: 13px;
|
|
|
|
| 441 |
}
|
| 442 |
|
| 443 |
.model-name {
|
| 444 |
font-weight: 500;
|
| 445 |
-
color:
|
| 446 |
white-space: nowrap;
|
| 447 |
overflow: hidden;
|
| 448 |
text-overflow: ellipsis;
|
| 449 |
max-width: 60%;
|
|
|
|
| 450 |
}
|
| 451 |
|
| 452 |
.model-count {
|
| 453 |
display: flex;
|
| 454 |
align-items: center;
|
| 455 |
gap: 8px;
|
| 456 |
-
color:
|
| 457 |
font-weight: 600;
|
|
|
|
| 458 |
}
|
| 459 |
|
| 460 |
.model-usage-text {
|
| 461 |
-
color:
|
| 462 |
font-weight: normal;
|
| 463 |
font-size: 12px;
|
|
|
|
| 464 |
}
|
| 465 |
|
| 466 |
.model-progress-container {
|
| 467 |
width: 60px;
|
| 468 |
height: 6px;
|
| 469 |
-
background-color:
|
| 470 |
border-radius: 3px;
|
| 471 |
overflow: hidden;
|
| 472 |
margin-left: 5px;
|
|
|
|
| 473 |
}
|
| 474 |
|
| 475 |
.model-progress-bar {
|
| 476 |
height: 100%;
|
| 477 |
border-radius: 3px;
|
| 478 |
-
transition: width 0.3s ease;
|
| 479 |
}
|
| 480 |
|
| 481 |
.view-more-models {
|
| 482 |
text-align: center;
|
| 483 |
-
color:
|
| 484 |
font-size: 12px;
|
| 485 |
cursor: pointer;
|
| 486 |
padding: 8px;
|
| 487 |
margin-top: 5px;
|
| 488 |
border-radius: 4px;
|
| 489 |
background-color: rgba(0, 123, 255, 0.05);
|
| 490 |
-
transition: all 0.2s ease;
|
| 491 |
}
|
| 492 |
|
| 493 |
.view-more-models:hover {
|
|
@@ -509,7 +524,7 @@
|
|
| 509 |
}
|
| 510 |
|
| 511 |
.fold-header:hover {
|
| 512 |
-
background-color:
|
| 513 |
}
|
| 514 |
|
| 515 |
.fold-icon {
|
|
@@ -544,10 +559,6 @@
|
|
| 544 |
}
|
| 545 |
|
| 546 |
/* 模型统计项目悬停效果 */
|
| 547 |
-
.model-stat-item {
|
| 548 |
-
transition: transform 0.2s, box-shadow 0.2s;
|
| 549 |
-
}
|
| 550 |
-
|
| 551 |
.model-stat-item:hover {
|
| 552 |
transform: translateY(-2px);
|
| 553 |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
|
|
| 159 |
|
| 160 |
<style scoped>
|
| 161 |
.info-box {
|
| 162 |
+
background-color: var(--card-background);
|
| 163 |
+
border: 1px solid var(--card-border);
|
| 164 |
border-radius: 8px;
|
| 165 |
padding: 20px;
|
| 166 |
margin-bottom: 20px;
|
| 167 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 168 |
+
transition: background-color 0.3s, border-color 0.3s, box-shadow 0.3s;
|
| 169 |
}
|
| 170 |
|
| 171 |
/* 移动端优化 - 减小外边距 */
|
|
|
|
| 190 |
}
|
| 191 |
|
| 192 |
.section-title {
|
| 193 |
+
color: var(--color-heading);
|
| 194 |
+
border-bottom: 1px solid var(--color-border);
|
| 195 |
padding-bottom: 10px;
|
| 196 |
margin-bottom: 20px;
|
| 197 |
+
transition: color 0.3s, border-color 0.3s;
|
| 198 |
}
|
| 199 |
|
| 200 |
.stats-grid {
|
|
|
|
| 213 |
}
|
| 214 |
|
| 215 |
.stat-card {
|
| 216 |
+
background-color: var(--stats-item-bg);
|
| 217 |
padding: 15px;
|
| 218 |
border-radius: 8px;
|
| 219 |
text-align: center;
|
| 220 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 221 |
+
transition: transform 0.2s, background-color 0.3s, box-shadow 0.3s;
|
| 222 |
}
|
| 223 |
|
| 224 |
.stat-card:hover {
|
|
|
|
| 229 |
.stat-value {
|
| 230 |
font-size: 24px;
|
| 231 |
font-weight: bold;
|
| 232 |
+
color: var(--button-primary);
|
| 233 |
white-space: nowrap;
|
| 234 |
overflow: hidden;
|
| 235 |
text-overflow: ellipsis;
|
| 236 |
+
transition: color 0.3s;
|
| 237 |
}
|
| 238 |
|
| 239 |
.stat-label {
|
| 240 |
font-size: 14px;
|
| 241 |
+
color: var(--color-text);
|
| 242 |
margin-top: 5px;
|
| 243 |
white-space: nowrap;
|
| 244 |
overflow: hidden;
|
| 245 |
text-overflow: ellipsis;
|
| 246 |
+
transition: color 0.3s;
|
| 247 |
}
|
| 248 |
|
| 249 |
/* 移动端优化 - 更紧凑的卡片 */
|
|
|
|
| 305 |
}
|
| 306 |
|
| 307 |
.api-key-item {
|
| 308 |
+
background-color: var(--stats-item-bg);
|
| 309 |
border-radius: 8px;
|
| 310 |
padding: 15px;
|
| 311 |
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 312 |
+
transition: background-color 0.3s, box-shadow 0.3s;
|
| 313 |
}
|
| 314 |
|
| 315 |
.api-key-header {
|
|
|
|
| 321 |
|
| 322 |
.api-key-name {
|
| 323 |
font-weight: bold;
|
| 324 |
+
color: var(--color-heading);
|
| 325 |
white-space: nowrap;
|
| 326 |
overflow: hidden;
|
| 327 |
text-overflow: ellipsis;
|
| 328 |
max-width: 50%;
|
| 329 |
+
transition: color 0.3s;
|
| 330 |
}
|
| 331 |
|
| 332 |
.api-key-usage {
|
|
|
|
| 338 |
|
| 339 |
.api-key-count {
|
| 340 |
font-weight: bold;
|
| 341 |
+
color: var(--button-primary);
|
| 342 |
+
transition: color 0.3s;
|
| 343 |
}
|
| 344 |
|
| 345 |
/* 移动端优化 - 更紧凑的API密钥项 */
|
|
|
|
| 382 |
.progress-container {
|
| 383 |
width: 100%;
|
| 384 |
height: 10px;
|
| 385 |
+
background-color: var(--color-background-soft);
|
| 386 |
border-radius: 5px;
|
| 387 |
overflow: hidden;
|
| 388 |
+
transition: background-color 0.3s;
|
| 389 |
}
|
| 390 |
|
| 391 |
.progress-bar {
|
| 392 |
height: 100%;
|
| 393 |
border-radius: 5px;
|
| 394 |
+
transition: width 0.3s ease, background-color 0.3s;
|
| 395 |
}
|
| 396 |
|
| 397 |
.progress-bar.low {
|
|
|
|
| 409 |
/* 模型统计样式 */
|
| 410 |
.model-stats-container {
|
| 411 |
margin-top: 10px;
|
| 412 |
+
border-top: 1px dashed var(--color-border);
|
| 413 |
padding-top: 10px;
|
| 414 |
+
transition: border-color 0.3s;
|
| 415 |
}
|
| 416 |
|
| 417 |
.model-stats-header {
|
|
|
|
| 421 |
cursor: pointer;
|
| 422 |
user-select: none;
|
| 423 |
margin-bottom: 8px;
|
| 424 |
+
color: var(--color-heading);
|
| 425 |
font-size: 14px;
|
| 426 |
+
transition: color 0.3s;
|
| 427 |
}
|
| 428 |
|
| 429 |
.model-stats-title {
|
|
|
|
| 445 |
justify-content: space-between;
|
| 446 |
align-items: center;
|
| 447 |
padding: 6px 10px;
|
| 448 |
+
background-color: var(--color-background-mute);
|
| 449 |
border-radius: 4px;
|
| 450 |
font-size: 13px;
|
| 451 |
+
transition: transform 0.2s, box-shadow 0.2s, background-color 0.3s;
|
| 452 |
}
|
| 453 |
|
| 454 |
.model-name {
|
| 455 |
font-weight: 500;
|
| 456 |
+
color: var(--color-heading);
|
| 457 |
white-space: nowrap;
|
| 458 |
overflow: hidden;
|
| 459 |
text-overflow: ellipsis;
|
| 460 |
max-width: 60%;
|
| 461 |
+
transition: color 0.3s;
|
| 462 |
}
|
| 463 |
|
| 464 |
.model-count {
|
| 465 |
display: flex;
|
| 466 |
align-items: center;
|
| 467 |
gap: 8px;
|
| 468 |
+
color: var(--button-primary);
|
| 469 |
font-weight: 600;
|
| 470 |
+
transition: color 0.3s;
|
| 471 |
}
|
| 472 |
|
| 473 |
.model-usage-text {
|
| 474 |
+
color: var(--color-text);
|
| 475 |
font-weight: normal;
|
| 476 |
font-size: 12px;
|
| 477 |
+
transition: color 0.3s;
|
| 478 |
}
|
| 479 |
|
| 480 |
.model-progress-container {
|
| 481 |
width: 60px;
|
| 482 |
height: 6px;
|
| 483 |
+
background-color: var(--color-background-soft);
|
| 484 |
border-radius: 3px;
|
| 485 |
overflow: hidden;
|
| 486 |
margin-left: 5px;
|
| 487 |
+
transition: background-color 0.3s;
|
| 488 |
}
|
| 489 |
|
| 490 |
.model-progress-bar {
|
| 491 |
height: 100%;
|
| 492 |
border-radius: 3px;
|
| 493 |
+
transition: width 0.3s ease, background-color 0.3s;
|
| 494 |
}
|
| 495 |
|
| 496 |
.view-more-models {
|
| 497 |
text-align: center;
|
| 498 |
+
color: var(--button-primary);
|
| 499 |
font-size: 12px;
|
| 500 |
cursor: pointer;
|
| 501 |
padding: 8px;
|
| 502 |
margin-top: 5px;
|
| 503 |
border-radius: 4px;
|
| 504 |
background-color: rgba(0, 123, 255, 0.05);
|
| 505 |
+
transition: all 0.2s ease, color 0.3s, background-color 0.3s;
|
| 506 |
}
|
| 507 |
|
| 508 |
.view-more-models:hover {
|
|
|
|
| 524 |
}
|
| 525 |
|
| 526 |
.fold-header:hover {
|
| 527 |
+
background-color: var(--color-background-mute);
|
| 528 |
}
|
| 529 |
|
| 530 |
.fold-icon {
|
|
|
|
| 559 |
}
|
| 560 |
|
| 561 |
/* 模型统计项目悬停效果 */
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
.model-stat-item:hover {
|
| 563 |
transform: translateY(-2px);
|
| 564 |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
hajimiUI/src/stores/dashboard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { defineStore } from 'pinia'
|
| 2 |
-
import { ref } from 'vue'
|
| 3 |
|
| 4 |
export const useDashboardStore = defineStore('dashboard', () => {
|
| 5 |
// 状态
|
|
@@ -31,6 +31,27 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
|
| 31 |
// 添加模型相关状态
|
| 32 |
const selectedModel = ref('all')
|
| 33 |
const availableModels = ref([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
// 获取仪表盘数据
|
| 36 |
async function fetchDashboardData() {
|
|
@@ -108,6 +129,11 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
|
| 108 |
selectedModel.value = model
|
| 109 |
}
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
return {
|
| 112 |
status,
|
| 113 |
config,
|
|
@@ -117,6 +143,8 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
|
| 117 |
fetchDashboardData,
|
| 118 |
selectedModel,
|
| 119 |
availableModels,
|
| 120 |
-
setSelectedModel
|
|
|
|
|
|
|
| 121 |
}
|
| 122 |
})
|
|
|
|
| 1 |
import { defineStore } from 'pinia'
|
| 2 |
+
import { ref, watch } from 'vue'
|
| 3 |
|
| 4 |
export const useDashboardStore = defineStore('dashboard', () => {
|
| 5 |
// 状态
|
|
|
|
| 31 |
// 添加模型相关状态
|
| 32 |
const selectedModel = ref('all')
|
| 33 |
const availableModels = ref([])
|
| 34 |
+
|
| 35 |
+
// 夜间模式状态
|
| 36 |
+
const isDarkMode = ref(localStorage.getItem('darkMode') === 'true')
|
| 37 |
+
|
| 38 |
+
// 监听夜间模式变化,保存到localStorage
|
| 39 |
+
watch(isDarkMode, (newValue) => {
|
| 40 |
+
localStorage.setItem('darkMode', newValue)
|
| 41 |
+
applyDarkMode(newValue)
|
| 42 |
+
})
|
| 43 |
+
|
| 44 |
+
// 应用夜间模式
|
| 45 |
+
function applyDarkMode(isDark) {
|
| 46 |
+
if (isDark) {
|
| 47 |
+
document.documentElement.classList.add('dark-mode')
|
| 48 |
+
} else {
|
| 49 |
+
document.documentElement.classList.remove('dark-mode')
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// 初始应用夜间模式
|
| 54 |
+
applyDarkMode(isDarkMode.value)
|
| 55 |
|
| 56 |
// 获取仪表盘数据
|
| 57 |
async function fetchDashboardData() {
|
|
|
|
| 129 |
selectedModel.value = model
|
| 130 |
}
|
| 131 |
|
| 132 |
+
// 切换夜间模式
|
| 133 |
+
function toggleDarkMode() {
|
| 134 |
+
isDarkMode.value = !isDarkMode.value
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
return {
|
| 138 |
status,
|
| 139 |
config,
|
|
|
|
| 143 |
fetchDashboardData,
|
| 144 |
selectedModel,
|
| 145 |
availableModels,
|
| 146 |
+
setSelectedModel,
|
| 147 |
+
isDarkMode,
|
| 148 |
+
toggleDarkMode
|
| 149 |
}
|
| 150 |
})
|
hajimiUI/src/views/DashboardView.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
<script setup>
|
| 2 |
-
import { ref, onMounted, onUnmounted } from 'vue'
|
| 3 |
import StatusSection from '../components/dashboard/StatusSection.vue'
|
| 4 |
import ConfigSection from '../components/dashboard/ConfigSection.vue'
|
| 5 |
import LogSection from '../components/dashboard/LogSection.vue'
|
|
@@ -8,6 +8,9 @@ import { useDashboardStore } from '../stores/dashboard'
|
|
| 8 |
const dashboardStore = useDashboardStore()
|
| 9 |
const refreshInterval = ref(null)
|
| 10 |
|
|
|
|
|
|
|
|
|
|
| 11 |
// 页面加载时获取数据并启动自动刷新
|
| 12 |
onMounted(() => {
|
| 13 |
fetchDashboardData()
|
|
@@ -45,11 +48,25 @@ async function fetchDashboardData() {
|
|
| 45 |
function handleRefresh() {
|
| 46 |
fetchDashboardData()
|
| 47 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
</script>
|
| 49 |
|
| 50 |
<template>
|
| 51 |
<div class="dashboard">
|
| 52 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
<!-- 运行状态部分 -->
|
| 55 |
<StatusSection />
|
|
@@ -68,9 +85,11 @@ function handleRefresh() {
|
|
| 68 |
body {
|
| 69 |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
| 70 |
line-height: 1.6;
|
| 71 |
-
background-color:
|
|
|
|
| 72 |
margin: 0;
|
| 73 |
padding: 0;
|
|
|
|
| 74 |
}
|
| 75 |
|
| 76 |
.dashboard {
|
|
@@ -79,21 +98,103 @@ body {
|
|
| 79 |
padding: 20px;
|
| 80 |
}
|
| 81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
h1 {
|
| 83 |
-
color:
|
| 84 |
-
|
| 85 |
-
margin: 20px 0;
|
| 86 |
font-size: 1.8rem;
|
| 87 |
}
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
/* 移动端优化 - 减小整体边距 */
|
| 90 |
@media (max-width: 768px) {
|
| 91 |
.dashboard {
|
| 92 |
padding: 10px 8px;
|
| 93 |
}
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
h1 {
|
| 96 |
-
|
|
|
|
|
|
|
| 97 |
}
|
| 98 |
}
|
| 99 |
|
|
@@ -103,7 +204,26 @@ h1 {
|
|
| 103 |
}
|
| 104 |
|
| 105 |
h1 {
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
}
|
| 108 |
}
|
| 109 |
|
|
@@ -111,8 +231,8 @@ h1 {
|
|
| 111 |
display: block;
|
| 112 |
margin: 20px auto;
|
| 113 |
padding: 10px 20px;
|
| 114 |
-
background-color:
|
| 115 |
-
color:
|
| 116 |
border: none;
|
| 117 |
border-radius: 4px;
|
| 118 |
font-size: 16px;
|
|
@@ -121,7 +241,7 @@ h1 {
|
|
| 121 |
}
|
| 122 |
|
| 123 |
.refresh-button:hover {
|
| 124 |
-
background-color:
|
| 125 |
}
|
| 126 |
|
| 127 |
/* 全局响应式样式 - 保持三栏布局但优化显示 */
|
|
@@ -131,12 +251,16 @@ h1 {
|
|
| 131 |
padding: 10px 6px;
|
| 132 |
margin-bottom: 10px;
|
| 133 |
border-radius: 6px;
|
|
|
|
|
|
|
| 134 |
}
|
| 135 |
|
| 136 |
:deep(.section-title) {
|
| 137 |
font-size: 1.1rem;
|
| 138 |
margin-bottom: 10px;
|
| 139 |
padding-bottom: 6px;
|
|
|
|
|
|
|
| 140 |
}
|
| 141 |
|
| 142 |
:deep(.stats-grid) {
|
|
|
|
| 1 |
<script setup>
|
| 2 |
+
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
| 3 |
import StatusSection from '../components/dashboard/StatusSection.vue'
|
| 4 |
import ConfigSection from '../components/dashboard/ConfigSection.vue'
|
| 5 |
import LogSection from '../components/dashboard/LogSection.vue'
|
|
|
|
| 8 |
const dashboardStore = useDashboardStore()
|
| 9 |
const refreshInterval = ref(null)
|
| 10 |
|
| 11 |
+
// 计算属性:夜间模式状态
|
| 12 |
+
const isDarkMode = computed(() => dashboardStore.isDarkMode)
|
| 13 |
+
|
| 14 |
// 页面加载时获取数据并启动自动刷新
|
| 15 |
onMounted(() => {
|
| 16 |
fetchDashboardData()
|
|
|
|
| 48 |
function handleRefresh() {
|
| 49 |
fetchDashboardData()
|
| 50 |
}
|
| 51 |
+
|
| 52 |
+
// 切换夜间模式
|
| 53 |
+
function toggleDarkMode() {
|
| 54 |
+
dashboardStore.toggleDarkMode()
|
| 55 |
+
}
|
| 56 |
</script>
|
| 57 |
|
| 58 |
<template>
|
| 59 |
<div class="dashboard">
|
| 60 |
+
<div class="header-container">
|
| 61 |
+
<h1>🤖 Gemini API 代理服务</h1>
|
| 62 |
+
<div class="theme-toggle">
|
| 63 |
+
<label class="switch">
|
| 64 |
+
<input type="checkbox" :checked="isDarkMode" @change="toggleDarkMode">
|
| 65 |
+
<span class="slider round"></span>
|
| 66 |
+
</label>
|
| 67 |
+
<span class="toggle-label">{{ isDarkMode ? '🌙' : '☀️' }}</span>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
|
| 71 |
<!-- 运行状态部分 -->
|
| 72 |
<StatusSection />
|
|
|
|
| 85 |
body {
|
| 86 |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
| 87 |
line-height: 1.6;
|
| 88 |
+
background-color: var(--color-background);
|
| 89 |
+
color: var(--color-text);
|
| 90 |
margin: 0;
|
| 91 |
padding: 0;
|
| 92 |
+
transition: background-color 0.3s, color 0.3s;
|
| 93 |
}
|
| 94 |
|
| 95 |
.dashboard {
|
|
|
|
| 98 |
padding: 20px;
|
| 99 |
}
|
| 100 |
|
| 101 |
+
.header-container {
|
| 102 |
+
display: flex;
|
| 103 |
+
justify-content: space-between;
|
| 104 |
+
align-items: center;
|
| 105 |
+
margin-bottom: 20px;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
h1 {
|
| 109 |
+
color: var(--color-heading);
|
| 110 |
+
margin: 0;
|
|
|
|
| 111 |
font-size: 1.8rem;
|
| 112 |
}
|
| 113 |
|
| 114 |
+
/* 主题切换开关 */
|
| 115 |
+
.theme-toggle {
|
| 116 |
+
display: flex;
|
| 117 |
+
align-items: center;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.toggle-label {
|
| 121 |
+
margin-left: 8px;
|
| 122 |
+
font-size: 1.2rem;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/* 开关样式 */
|
| 126 |
+
.switch {
|
| 127 |
+
position: relative;
|
| 128 |
+
display: inline-block;
|
| 129 |
+
width: 60px;
|
| 130 |
+
height: 30px;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.switch input {
|
| 134 |
+
opacity: 0;
|
| 135 |
+
width: 0;
|
| 136 |
+
height: 0;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.slider {
|
| 140 |
+
position: absolute;
|
| 141 |
+
cursor: pointer;
|
| 142 |
+
top: 0;
|
| 143 |
+
left: 0;
|
| 144 |
+
right: 0;
|
| 145 |
+
bottom: 0;
|
| 146 |
+
background-color: var(--toggle-bg);
|
| 147 |
+
transition: .4s;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.slider:before {
|
| 151 |
+
position: absolute;
|
| 152 |
+
content: "";
|
| 153 |
+
height: 22px;
|
| 154 |
+
width: 22px;
|
| 155 |
+
left: 4px;
|
| 156 |
+
bottom: 4px;
|
| 157 |
+
background-color: white;
|
| 158 |
+
transition: .4s;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
input:checked + .slider {
|
| 162 |
+
background-color: var(--toggle-active);
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
input:focus + .slider {
|
| 166 |
+
box-shadow: 0 0 1px var(--toggle-active);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
input:checked + .slider:before {
|
| 170 |
+
transform: translateX(30px);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.slider.round {
|
| 174 |
+
border-radius: 34px;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.slider.round:before {
|
| 178 |
+
border-radius: 50%;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
/* 移动端优化 - 减小整体边距 */
|
| 182 |
@media (max-width: 768px) {
|
| 183 |
.dashboard {
|
| 184 |
padding: 10px 8px;
|
| 185 |
}
|
| 186 |
|
| 187 |
+
.header-container {
|
| 188 |
+
flex-direction: row;
|
| 189 |
+
justify-content: space-between;
|
| 190 |
+
align-items: center;
|
| 191 |
+
margin-bottom: 15px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
h1 {
|
| 195 |
+
font-size: 1.4rem;
|
| 196 |
+
text-align: left;
|
| 197 |
+
margin-right: 10px;
|
| 198 |
}
|
| 199 |
}
|
| 200 |
|
|
|
|
| 204 |
}
|
| 205 |
|
| 206 |
h1 {
|
| 207 |
+
font-size: 1.2rem;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.switch {
|
| 211 |
+
width: 50px;
|
| 212 |
+
height: 26px;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.slider:before {
|
| 216 |
+
height: 18px;
|
| 217 |
+
width: 18px;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
input:checked + .slider:before {
|
| 221 |
+
transform: translateX(24px);
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.toggle-label {
|
| 225 |
+
margin-left: 5px;
|
| 226 |
+
font-size: 1rem;
|
| 227 |
}
|
| 228 |
}
|
| 229 |
|
|
|
|
| 231 |
display: block;
|
| 232 |
margin: 20px auto;
|
| 233 |
padding: 10px 20px;
|
| 234 |
+
background-color: var(--button-primary);
|
| 235 |
+
color: var(--button-text);
|
| 236 |
border: none;
|
| 237 |
border-radius: 4px;
|
| 238 |
font-size: 16px;
|
|
|
|
| 241 |
}
|
| 242 |
|
| 243 |
.refresh-button:hover {
|
| 244 |
+
background-color: var(--button-primary-hover);
|
| 245 |
}
|
| 246 |
|
| 247 |
/* 全局响应式样式 - 保持三栏布局但优化显示 */
|
|
|
|
| 251 |
padding: 10px 6px;
|
| 252 |
margin-bottom: 10px;
|
| 253 |
border-radius: 6px;
|
| 254 |
+
background-color: var(--card-background);
|
| 255 |
+
border: 1px solid var(--card-border);
|
| 256 |
}
|
| 257 |
|
| 258 |
:deep(.section-title) {
|
| 259 |
font-size: 1.1rem;
|
| 260 |
margin-bottom: 10px;
|
| 261 |
padding-bottom: 6px;
|
| 262 |
+
color: var(--color-heading);
|
| 263 |
+
border-bottom: 1px solid var(--color-border);
|
| 264 |
}
|
| 265 |
|
| 266 |
:deep(.stats-grid) {
|
hajimiUI/vite.config.js
CHANGED
|
@@ -11,7 +11,7 @@ const __dirname = dirname(__filename);
|
|
| 11 |
|
| 12 |
// https://vite.dev/config/
|
| 13 |
export default defineConfig({
|
| 14 |
-
base: '/
|
| 15 |
plugins: [
|
| 16 |
vue(),
|
| 17 |
vueDevTools(),
|
|
|
|
| 11 |
|
| 12 |
// https://vite.dev/config/
|
| 13 |
export default defineConfig({
|
| 14 |
+
base: '/',
|
| 15 |
plugins: [
|
| 16 |
vue(),
|
| 17 |
vueDevTools(),
|
readme.md
CHANGED
|
@@ -8,8 +8,13 @@
|
|
| 8 |
- [termux部署的使用文档(手机使用)](./wiki/Termux.md)
|
| 9 |
- [docker部署的使用文档(服务器自建使用)](./wiki/docker.md)
|
| 10 |
### 更新日志
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
* v0.1.0
|
| 12 |
* 使用vue重写前端界面,适配移动端
|
|
|
|
| 13 |
* 支持为多模态模型上传图片
|
| 14 |
* 可用秘钥数量将异步更新,防止阻塞进程
|
| 15 |
* 这次真能北京时间16点自动重置统计数据了
|
|
|
|
| 8 |
- [termux部署的使用文档(手机使用)](./wiki/Termux.md)
|
| 9 |
- [docker部署的使用文档(服务器自建使用)](./wiki/docker.md)
|
| 10 |
### 更新日志
|
| 11 |
+
* v0.1.1
|
| 12 |
+
* 新增联网模式,为所有gemini2.x模型提供联网能力,在模型列表中选择-serach后缀的模型启用
|
| 13 |
+
* 新增环境变量`SERACH_MODE`是否启用联网模式,默认为true
|
| 14 |
+
* 新增环境变量`SERACH_PROMPT`为联网模式提示词,默认为`(使用搜索工具联网搜索,需要在content中结合搜索内容)`
|
| 15 |
* v0.1.0
|
| 16 |
* 使用vue重写前端界面,适配移动端
|
| 17 |
+
* 前端界面添加黑夜模式
|
| 18 |
* 支持为多模态模型上传图片
|
| 19 |
* 可用秘钥数量将异步更新,防止阻塞进程
|
| 20 |
* 这次真能北京时间16点自动重置统计数据了
|
version.txt
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
version=0.1.
|
|
|
|
| 1 |
+
version=0.1.1
|