linchuans commited on
Commit
a8e8f36
·
verified ·
1 Parent(s): 9333b9c

Upload 76 files

Browse files
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 .logging_utils import log
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 .logging_utils import log
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', "非流式请求成功完成", extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
 
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请求在客户端断开后完成", extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
 
48
  return response_content
49
  except Exception as e:
50
- extra_log_gemini_cancel = {'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model, 'error_message': f'API请求在客户端断开失败: {str(e)}'}
51
- log('info', "API调用因客户端断开失败", extra=extra_log_gemini_cancel)
52
  raise
53
 
54
  # 如果任务尚未开始或已经失败,记录日志
55
- extra_log_gemini_cancel = {'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model, 'error_message': '客户端断开导致API调用取消'}
56
- log('info', "API调用因客户端断开取消", extra=extra_log_gemini_cancel)
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 .logging_utils import log
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 .logging_utils import log
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
- error_msg = "无效的模型"
39
- extra_log = {'request_type': request_type, 'model': chat_request.model, 'status_code': 400, 'error_message': error_msg}
40
- log('error', error_msg, extra=extra_log)
41
  raise HTTPException(
42
- status_code=status.HTTP_400_BAD_REQUEST, detail=error_msg)
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, 'status_code': 'N/A'})
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('error', f"流式请求失败: {error_detail}",
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('error', "客户端连接中断",
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={'key': 'N/A', 'request_type': 'switch_key', 'status_code': 'N/A'})
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
- generate_cache_key,
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
- import logging
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 .logging_utils import log
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
- extra_log_cancel = {'key': current_api_key[:8], 'request_type': 'stream', 'model': chat_request.model, 'error_message': '客户端已断开连接'}
266
- log('info', "客户端连接已中断", extra=extra_log_cancel)
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
- return part['text']
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
- log_msg = format_log_message('INFO', "使用假流式请求模式(发送换行符保持连接)", extra=extra_log)
135
- logger.info(log_msg)
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
- log_msg = format_log_message('WARNING', "假流式请求等待时间过长,强制结束", extra=extra_log)
152
- logger.warning(log_msg)
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
- error_msg = f"假流式处理期间发生错误: {str(e)}"
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
- log_msg = format_log_message('INFO', "假流式请求结束", extra=extra_log)
169
- logger.info(log_msg)
170
  else:
171
- # 原始流式请求处理逻辑
172
- api_version = "v1alpha" if "think" in request.model else "v1beta"
173
- url = f"https://generativelanguage.googleapis.com/{api_version}/models/{request.model}:streamGenerateContent?key={self.api_key}&alt=sse"
 
 
 
 
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, 'status_code': 'N/A'}
245
- log_msg = format_log_message('INFO', "非流式请求开始", extra=extra_log)
246
- logger.info(log_msg)
247
 
248
- api_version = "v1alpha" if "think" in request.model else "v1beta"
249
- url = f"https://generativelanguage.googleapis.com/{api_version}/models/{request.model}:generateContent?key={self.api_key}"
 
250
  headers = {
251
  "Content-Type": "application/json",
252
  }
253
- data = {
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 = [model["name"] for model in data.get("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="/assets/favicon.ico">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <title>HAJIMI</title>
8
- <script type="module" crossorigin src="/assets/main.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index.css">
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
- if not self.api_keys:
40
- log_msg = format_log_message('ERROR', "没有配置任何 API 密钥!")
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错误,对500和503错误实现自动重试机制"""
107
- error_detail = handle_gemini_error(e, api_key, key_manager)
108
 
109
- # 处理500和503服务器错误
110
- if isinstance(e, requests.exceptions.HTTPError) and ('500' in str(e) or '503' in str(e)):
111
- status_code = '500' if '500' in str(e) else '503'
112
-
113
- # 最多重试3次
114
- if retry_count < 3:
115
- wait_time = min(1 * (2 ** retry_count), 16) # RETRY_DELAY=1, MAX_RETRY_DELAY=16
116
- log_msg = format_log_message('WARNING', f"Gemini服务器错误({status_code}),等待{wait_time}秒后重试 ({retry_count+1}/3)",
 
117
  extra={'key': api_key[:8], 'request_type': request_type, 'model': model, 'status_code': int(status_code)})
118
- logger.warning(log_msg)
119
 
120
  # 等待后返回重试信号
121
  await asyncio.sleep(wait_time)
122
- return {'should_retry': True, 'error': error_detail, 'remove_cache': False}
123
 
124
- # 重试次数用尽,直接返回错误状态码
125
- log_msg = format_log_message('ERROR', f"服务器错误({status_code})重试{retry_count}次后仍然失败",
126
- extra={'key': api_key[:8], 'request_type': request_type, 'model': model, 'status_code': int(status_code)})
127
- logger.error(log_msg)
128
 
129
- # 不建议切换密钥,直接抛出HTTP异常
130
  raise HTTPException(status_code=int(status_code),
131
  detail=f"Gemini API 服务器错误({status_code}),请稍后重试")
132
 
133
- # 对于其他错误,返回切换密钥的信号
134
- log_msg = format_log_message('ERROR', f"API错误: {error_detail}",
135
- extra={'key': api_key[:8], 'request_type': request_type, 'model': model, 'error_message': error_detail})
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
+ # 500503错误实现自动重试机制, 最多重试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: #fff;
72
- border: 1px solid #dee2e6;
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: #495057;
94
- border-bottom: 1px solid #dee2e6;
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: #e9ecef;
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: #007bff;
132
  white-space: nowrap;
133
  overflow: hidden;
134
  text-overflow: ellipsis;
 
135
  }
136
 
137
  .stat-label {
138
  font-size: 14px;
139
- color: #6c757d;
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: #fff;
61
- border: 1px solid #dee2e6;
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: #495057;
83
- border-bottom: 1px solid #dee2e6;
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 #ddd;
99
  border-radius: 4px;
100
- background-color: #f8f9fa;
 
101
  cursor: pointer;
102
  min-width: 60px;
 
103
  }
104
 
105
  .log-filter button.active {
106
- background-color: #007bff;
107
- color: white;
108
- border-color: #007bff;
109
  }
110
 
111
  /* 移动端优化 */
@@ -137,8 +141,8 @@ watch(() => dashboardStore.logs, async () => {
137
  }
138
 
139
  .log-container {
140
- background-color: #f5f5f5;
141
- border: 1px solid #ddd;
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: #e8f4f8;
161
  border-left: 4px solid #17a2b8;
162
  }
163
 
164
  .log-entry.WARNING {
165
- background-color: #fff3cd;
166
  border-left: 4px solid #ffc107;
167
  }
168
 
169
  .log-entry.ERROR {
170
- background-color: #f8d7da;
171
  border-left: 4px solid #dc3545;
172
  }
173
 
174
  .log-entry.DEBUG {
175
- background-color: #d1ecf1;
176
  border-left: 4px solid #17a2b8;
177
  }
178
 
179
  .log-timestamp {
180
- color: #6c757d;
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: #212529;
 
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: #fff;
163
- border: 1px solid #dee2e6;
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: #495057;
193
- border-bottom: 1px solid #dee2e6;
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: #e9ecef;
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: #007bff;
231
  white-space: nowrap;
232
  overflow: hidden;
233
  text-overflow: ellipsis;
 
234
  }
235
 
236
  .stat-label {
237
  font-size: 14px;
238
- color: #6c757d;
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: #f8f9fa;
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: #495057;
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: #007bff;
 
336
  }
337
 
338
  /* 移动端优化 - 更紧凑的API密钥项 */
@@ -375,15 +382,16 @@
375
  .progress-container {
376
  width: 100%;
377
  height: 10px;
378
- background-color: #e9ecef;
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 #dee2e6;
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: #495057;
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: #f1f3f5;
439
  border-radius: 4px;
440
  font-size: 13px;
 
441
  }
442
 
443
  .model-name {
444
  font-weight: 500;
445
- color: #495057;
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: #007bff;
457
  font-weight: 600;
 
458
  }
459
 
460
  .model-usage-text {
461
- color: #6c757d;
462
  font-weight: normal;
463
  font-size: 12px;
 
464
  }
465
 
466
  .model-progress-container {
467
  width: 60px;
468
  height: 6px;
469
- background-color: #e9ecef;
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: #007bff;
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: rgba(0, 0, 0, 0.03);
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
- <h1>🤖 Gemini API 代理服务</h1>
 
 
 
 
 
 
 
 
 
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: #f8f9fa;
 
72
  margin: 0;
73
  padding: 0;
 
74
  }
75
 
76
  .dashboard {
@@ -79,21 +98,103 @@ body {
79
  padding: 20px;
80
  }
81
 
 
 
 
 
 
 
 
82
  h1 {
83
- color: #333;
84
- text-align: center;
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
- margin: 12px 0 10px;
 
 
97
  }
98
  }
99
 
@@ -103,7 +204,26 @@ h1 {
103
  }
104
 
105
  h1 {
106
- margin: 8px 0 6px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  }
108
  }
109
 
@@ -111,8 +231,8 @@ h1 {
111
  display: block;
112
  margin: 20px auto;
113
  padding: 10px 20px;
114
- background-color: #007bff;
115
- color: white;
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: #0069d9;
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: '/assets/', // 设置基础路径为 /assets/
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.0
 
1
+ version=0.1.1