Dearyou commited on
Commit
79ac584
·
verified ·
1 Parent(s): 6b8248d

Upload 29 files

Browse files
hajimi.v0.0.4/Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # 安装 unzip 工具
6
+ RUN apt-get update && apt-get install -y unzip && rm -rf /var/lib/apt/lists/*
7
+
8
+ COPY app.zip .
9
+ COPY requirements.txt .
10
+ COPY version.txt .
11
+ RUN mkdir -p app
12
+ # 解压 app.zip 文件
13
+ RUN unzip app.zip -d app && rm app.zip
14
+
15
+ RUN pip install --no-cache-dir -r requirements.txt
16
+
17
+ # 环境变量 (在 Hugging Face Spaces 中设置)
18
+ # ENV GEMINI_API_KEYS=your_key_1,your_key_2,your_key_3
19
+
20
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
hajimi.v0.0.4/app.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:36d2a51720d43f678ef4456fb3101a41bf6dba3da5bc48d8bc7004db2c28addc
3
+ size 40675
hajimi.v0.0.4/app/__init__.py ADDED
File without changes
hajimi.v0.0.4/app/api/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from app.api.routes import router, init_router
2
+ from app.api.dashboard import dashboard_router, init_dashboard_router
3
+
4
+ __all__ = [
5
+ 'router',
6
+ 'init_router',
7
+ 'dashboard_router',
8
+ 'init_dashboard_router'
9
+ ]
hajimi.v0.0.4/app/api/dashboard.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from datetime import datetime, timedelta
3
+ from app.utils import (
4
+ log_manager,
5
+ ResponseCacheManager,
6
+ ActiveRequestsManager,
7
+ clean_expired_stats
8
+ )
9
+ from app.config.settings import (
10
+ api_call_stats,
11
+ client_request_history,
12
+ API_KEY_DAILY_LIMIT
13
+ )
14
+ from app.services import GeminiClient
15
+
16
+ # 创建路由器
17
+ dashboard_router = APIRouter(prefix="/api", tags=["dashboard"])
18
+
19
+ # 全局变量引用,将在init_dashboard_router中设置
20
+ key_manager = None
21
+ response_cache_manager = None
22
+ active_requests_manager = None
23
+
24
+ def init_dashboard_router(
25
+ key_mgr,
26
+ cache_mgr,
27
+ active_req_mgr
28
+ ):
29
+ """初始化仪表盘路由器"""
30
+ global key_manager, response_cache_manager, active_requests_manager
31
+ key_manager = key_mgr
32
+ response_cache_manager = cache_mgr
33
+ active_requests_manager = active_req_mgr
34
+ return dashboard_router
35
+
36
+ @dashboard_router.get("/dashboard-data")
37
+ async def get_dashboard_data():
38
+ """获取仪表盘数据的API端点,用于动态刷新"""
39
+ # 先清理过期数据,确保统计数据是最新的
40
+ clean_expired_stats(api_call_stats)
41
+ response_cache_manager.clean_expired() # 使用管理器清理缓存
42
+ active_requests_manager.clean_completed() # 使用管理器清理活跃请求
43
+
44
+ # 获取当前统计数据
45
+ now = datetime.now()
46
+
47
+ # 计算过去24小时的调用总数
48
+ last_24h_calls = sum(api_call_stats['last_24h']['total'].values())
49
+
50
+ # 计算过去一小时内的调用总数
51
+ one_hour_ago = now - timedelta(hours=1)
52
+ hourly_calls = 0
53
+ for hour_key, count in api_call_stats['hourly']['total'].items():
54
+ try:
55
+ hour_time = datetime.strptime(hour_key, '%Y-%m-%d %H:00')
56
+ if hour_time >= one_hour_ago:
57
+ hourly_calls += count
58
+ except ValueError:
59
+ continue
60
+
61
+ # 计算过去一分钟内的调用总数
62
+ one_minute_ago = now - timedelta(minutes=1)
63
+ minute_calls = 0
64
+ for minute_key, count in api_call_stats['minute']['total'].items():
65
+ try:
66
+ minute_time = datetime.strptime(minute_key, '%Y-%m-%d %H:%M')
67
+ if minute_time >= one_minute_ago:
68
+ minute_calls += count
69
+ except ValueError:
70
+ continue
71
+
72
+ # 获取API密钥使用统计
73
+ api_key_stats = []
74
+ for api_key in key_manager.api_keys:
75
+ # 获取API密钥前8位作为标识
76
+ api_key_id = api_key[:8]
77
+
78
+ # 计算24小时内的调用次数
79
+ calls_24h = 0
80
+ if 'by_endpoint' in api_call_stats['last_24h'] and api_key in api_call_stats['last_24h']['by_endpoint']:
81
+ calls_24h = sum(api_call_stats['last_24h']['by_endpoint'][api_key].values())
82
+
83
+ # 计算使用百分比
84
+ usage_percent = (calls_24h / API_KEY_DAILY_LIMIT) * 100 if API_KEY_DAILY_LIMIT > 0 else 0
85
+
86
+ # 添加到结果列表
87
+ api_key_stats.append({
88
+ 'api_key': api_key_id,
89
+ 'calls_24h': calls_24h,
90
+ 'limit': API_KEY_DAILY_LIMIT,
91
+ 'usage_percent': round(usage_percent, 2)
92
+ })
93
+
94
+ # 按使用百分比降序排序
95
+ api_key_stats.sort(key=lambda x: x['usage_percent'], reverse=True)
96
+
97
+ # 获取最近的日志
98
+ recent_logs = log_manager.get_recent_logs(50) # 获取最近50条日志
99
+
100
+ # 返回JSON格式的数据
101
+ return {
102
+ "key_count": len(key_manager.api_keys),
103
+ "model_count": len(GeminiClient.AVAILABLE_MODELS),
104
+ "retry_count": len(key_manager.api_keys),
105
+ "last_24h_calls": last_24h_calls,
106
+ "hourly_calls": hourly_calls,
107
+ "minute_calls": minute_calls,
108
+ "current_time": datetime.now().strftime('%H:%M:%S'),
109
+ "logs": recent_logs,
110
+ "api_key_stats": api_key_stats
111
+ }
hajimi.v0.0.4/app/api/routes.py ADDED
@@ -0,0 +1,918 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 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, ResponseWrapper
5
+ from app.utils import (
6
+ handle_gemini_error,
7
+ protect_from_abuse,
8
+ APIKeyManager,
9
+ test_api_key,
10
+ format_log_message,
11
+ log_manager,
12
+ generate_cache_key,
13
+ cache_response,
14
+ create_chat_response,
15
+ create_error_response,
16
+ handle_api_error,
17
+ update_api_call_stats
18
+ )
19
+ import json
20
+ import asyncio
21
+ import time
22
+ import logging
23
+ import random
24
+ from typing import Literal
25
+ from app.config.settings import (
26
+ api_call_stats,
27
+ BLOCKED_MODELS
28
+ )
29
+ # 获取logger
30
+ logger = logging.getLogger("my_logger")
31
+
32
+ # 创建路由器
33
+ router = APIRouter()
34
+
35
+ # 全局变量引用 - 这些将在main.py中初始化并传递给路由
36
+ key_manager = None
37
+ response_cache_manager = None
38
+ active_requests_manager = None
39
+ safety_settings = None
40
+ safety_settings_g2 = None
41
+ current_api_key = None
42
+ FAKE_STREAMING = None
43
+ FAKE_STREAMING_INTERVAL = None
44
+ PASSWORD = None
45
+ MAX_REQUESTS_PER_MINUTE = None
46
+ MAX_REQUESTS_PER_DAY_PER_IP = None
47
+
48
+ # 初始化路由器的函数
49
+ def init_router(
50
+ _key_manager,
51
+ _response_cache_manager,
52
+ _active_requests_manager,
53
+ _safety_settings,
54
+ _safety_settings_g2,
55
+ _current_api_key,
56
+ _fake_streaming,
57
+ _fake_streaming_interval,
58
+ _password,
59
+ _max_requests_per_minute,
60
+ _max_requests_per_day_per_ip
61
+ ):
62
+ global key_manager, response_cache_manager, active_requests_manager
63
+ global safety_settings, safety_settings_g2, current_api_key
64
+ global FAKE_STREAMING, FAKE_STREAMING_INTERVAL
65
+ global PASSWORD, MAX_REQUESTS_PER_MINUTE, MAX_REQUESTS_PER_DAY_PER_IP
66
+
67
+ key_manager = _key_manager
68
+ response_cache_manager = _response_cache_manager
69
+ active_requests_manager = _active_requests_manager
70
+ safety_settings = _safety_settings
71
+ safety_settings_g2 = _safety_settings_g2
72
+ current_api_key = _current_api_key
73
+ FAKE_STREAMING = _fake_streaming
74
+ FAKE_STREAMING_INTERVAL = _fake_streaming_interval
75
+ PASSWORD = _password
76
+ MAX_REQUESTS_PER_MINUTE = _max_requests_per_minute
77
+ MAX_REQUESTS_PER_DAY_PER_IP = _max_requests_per_day_per_ip
78
+
79
+ # 日志记录函数
80
+ def log(level: str, message: str, **extra):
81
+ """简化日志记录的统一函数"""
82
+ msg = format_log_message(level.upper(), message, extra=extra)
83
+ getattr(logger, level.lower())(msg)
84
+
85
+ # 密码验证依赖
86
+ async def verify_password(request: Request):
87
+ if PASSWORD:
88
+ auth_header = request.headers.get("Authorization")
89
+ if not auth_header or not auth_header.startswith("Bearer "):
90
+ raise HTTPException(
91
+ status_code=401, detail="Unauthorized: Missing or invalid token")
92
+ token = auth_header.split(" ")[1]
93
+ if token != PASSWORD:
94
+ raise HTTPException(
95
+ status_code=401, detail="Unauthorized: Invalid token")
96
+
97
+ # API路由
98
+ @router.get("/v1/models", response_model=ModelList)
99
+ def list_models():
100
+ log('info', "Received request to list models", extra={'request_type': 'list_models', 'status_code': 200})
101
+ filtered_models = [model for model in GeminiClient.AVAILABLE_MODELS if model not in BLOCKED_MODELS]
102
+ return ModelList(data=[{"id": model, "object": "model", "created": 1678888888, "owned_by": "organization-owner"} for model in filtered_models])
103
+
104
+ @router.post("/v1/chat/completions", response_model=ChatCompletionResponse)
105
+ async def chat_completions(request: ChatCompletionRequest, http_request: Request, _: None = Depends(verify_password)):
106
+ # 获取客户端IP
107
+ client_ip = http_request.client.host if http_request.client else "unknown"
108
+
109
+ # 流式请求直接处理,不使用缓存
110
+ if request.stream:
111
+ return await process_request(request, http_request, "stream")
112
+
113
+ # 生成完整缓存键 - 用于精确匹配
114
+ cache_key = generate_cache_key(request)
115
+
116
+ # 记录请求缓存键信息
117
+ log('info', f"请求缓存键: {cache_key[:8]}...",
118
+ extra={'cache_key': cache_key[:8], 'request_type': 'non-stream'})
119
+
120
+ # 检查精确缓存是否存在且未过期
121
+ cached_response, cache_hit = response_cache_manager.get(cache_key)
122
+ if cache_hit:
123
+ # 精确缓存命中
124
+ log('info', f"精确缓存命中: {cache_key[:8]}...",
125
+ extra={'cache_operation': 'hit', 'request_type': 'non-stream'})
126
+
127
+ # 同时清理相关的活跃任务,避免后续请求等待已经不需要的任务
128
+ active_requests_manager.remove_by_prefix(f"cache:{cache_key}")
129
+
130
+ # 安全删除缓存
131
+ if cache_key in response_cache_manager.cache:
132
+ del response_cache_manager.cache[cache_key]
133
+ log('info', f"缓存使用后已删除: {cache_key[:8]}...",
134
+ extra={'cache_operation': 'used-and-removed', 'request_type': 'non-stream'})
135
+
136
+ # 返回缓存响应
137
+ return cached_response
138
+
139
+ # 构建包含缓存键的活跃请求池键
140
+ pool_key = f"cache:{cache_key}"
141
+
142
+ # 查找所有使用相同缓存键的活跃任务
143
+ active_task = active_requests_manager.get(pool_key)
144
+ if active_task and not active_task.done():
145
+ log('info', f"发现相同请求的进行中任务",
146
+ extra={'request_type': 'non-stream', 'model': request.model})
147
+
148
+ # 等待已有任务完成
149
+ try:
150
+ # 设置超时,避免无限等待
151
+ await asyncio.wait_for(active_task, timeout=180)
152
+
153
+ # 通过缓存管理器获取已完成任务的结果
154
+ cached_response, cache_hit = response_cache_manager.get(cache_key)
155
+ if cache_hit:
156
+ # 安全删除缓存
157
+ if cache_key in response_cache_manager.cache:
158
+ del response_cache_manager.cache[cache_key]
159
+ log('info', f"使用已完成任务的缓存后删除: {cache_key[:8]}...",
160
+ extra={'cache_operation': 'used-and-removed', 'request_type': 'non-stream'})
161
+
162
+ return cached_response
163
+
164
+ # 如果缓存已被清除或不存在,使用任务结果
165
+ if active_task.done() and not active_task.cancelled():
166
+ result = active_task.result()
167
+ if result:
168
+ # 使用原始结果时,我们需要创建一个新的响应对象
169
+ # 避免使用可能已被其他请求修改的对象
170
+ new_response = ChatCompletionResponse(
171
+ id=f"chatcmpl-{int(time.time()*1000)}",
172
+ object="chat.completion",
173
+ created=int(time.time()),
174
+ model=result.model,
175
+ choices=result.choices
176
+ )
177
+
178
+ # 不要缓存此结果,因为它很可能是一个已存在但被使用后清除的缓存
179
+ return new_response
180
+ except (asyncio.TimeoutError, asyncio.CancelledError) as e:
181
+ # 任务超时或被取消的情况下,记录日志然后让代码继续执行
182
+ error_type = "超时" if isinstance(e, asyncio.TimeoutError) else "被取消"
183
+ log('warning', f"等待已有任务{error_type}: {pool_key}",
184
+ extra={'request_type': 'non-stream', 'model': request.model})
185
+
186
+ # 从活跃请求池移除该任务
187
+ if active_task.done() or active_task.cancelled():
188
+ active_requests_manager.remove(pool_key)
189
+ log('info', f"已从活跃请求池移除{error_type}任务: {pool_key}",
190
+ extra={'request_type': 'non-stream'})
191
+
192
+ # 创建请求处理任务
193
+ process_task = asyncio.create_task(
194
+ process_request(request, http_request, "non-stream", cache_key=cache_key, client_ip=client_ip)
195
+ )
196
+
197
+ # 将任务添加到活跃请求池
198
+ active_requests_manager.add(pool_key, process_task)
199
+
200
+ # 等待任务完成
201
+ try:
202
+ response = await process_task
203
+ return response
204
+ except Exception as e:
205
+ # 如果任务失败,从活跃请求池中移除
206
+ active_requests_manager.remove(pool_key)
207
+
208
+ # 检查是否已有缓存的结果(可能是由另一个任务创建的)
209
+ cached_response, cache_hit = response_cache_manager.get(cache_key)
210
+ if cache_hit:
211
+ log('info', f"任务失败但找到缓存,使用缓存结果: {cache_key[:8]}...",
212
+ extra={'request_type': 'non-stream', 'model': request.model})
213
+ return cached_response
214
+
215
+ # 重新抛出异常
216
+ raise
217
+
218
+ # 请求处理函数
219
+ async def process_request(chat_request: ChatCompletionRequest, http_request: Request, request_type: Literal['stream', 'non-stream'], cache_key: str = None, client_ip: str = None):
220
+ """处理API请求的主函数,根据需要处理流式或非流式请求"""
221
+ global current_api_key
222
+
223
+ # 请求前基本检查
224
+ protect_from_abuse(
225
+ http_request, MAX_REQUESTS_PER_MINUTE, MAX_REQUESTS_PER_DAY_PER_IP)
226
+ if chat_request.model not in GeminiClient.AVAILABLE_MODELS:
227
+ error_msg = "无效的模型"
228
+ extra_log = {'request_type': request_type, 'model': chat_request.model, 'status_code': 400, 'error_message': error_msg}
229
+ log('error', error_msg, extra=extra_log)
230
+ raise HTTPException(
231
+ status_code=status.HTTP_400_BAD_REQUEST, detail=error_msg)
232
+
233
+ # 重置已尝试的密钥
234
+ key_manager.reset_tried_keys_for_request()
235
+
236
+ # 转换消息格���
237
+ contents, system_instruction = GeminiClient.convert_messages(
238
+ GeminiClient, chat_request.messages)
239
+
240
+ # 设置重试次数(使用可用API密钥数量作为最大重试次数)
241
+ retry_attempts = len(key_manager.api_keys) if key_manager.api_keys else 1
242
+
243
+ # 尝试使用不同API密钥
244
+ for attempt in range(1, retry_attempts + 1):
245
+ # 获取下一个密钥
246
+ current_api_key = key_manager.get_available_key()
247
+
248
+ # 检查API密钥是否可用
249
+ if current_api_key is None:
250
+ log('warning', "没有可用的 API 密钥,跳过本次尝试",
251
+ extra={'request_type': request_type, 'model': chat_request.model, 'status_code': 'N/A'})
252
+ break
253
+
254
+ # 记录当前尝试的密钥信息
255
+ log('info', f"第 {attempt}/{retry_attempts} 次尝试 ... 使用密钥: {current_api_key[:8]}...",
256
+ extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
257
+
258
+ # 服务器错误重试逻辑
259
+ server_error_retries = 3
260
+ for server_retry in range(1, server_error_retries + 1):
261
+ try:
262
+ # 根据请求类型分别处理
263
+ if chat_request.stream:
264
+ try:
265
+ return await process_stream_request(
266
+ chat_request,
267
+ http_request,
268
+ contents,
269
+ system_instruction,
270
+ current_api_key
271
+ )
272
+ except Exception as e:
273
+ # 捕获流式请求的异常,但不立即返回错误
274
+ # 记录错误并继续尝试下一个API密钥
275
+ error_detail = handle_gemini_error(e, current_api_key, key_manager)
276
+ log('error', f"流式请求失败: {error_detail}",
277
+ extra={'key': current_api_key[:8], 'request_type': 'stream', 'model': chat_request.model})
278
+ # 不返回错误,而是抛出异常让外层循环处理
279
+ raise
280
+ else:
281
+ return await process_nonstream_request(
282
+ chat_request,
283
+ http_request,
284
+ request_type,
285
+ contents,
286
+ system_instruction,
287
+ current_api_key,
288
+ cache_key,
289
+ client_ip
290
+ )
291
+ except HTTPException as e:
292
+ if e.status_code == status.HTTP_408_REQUEST_TIMEOUT:
293
+ log('error', "客户端连接中断",
294
+ extra={'key': current_api_key[:8], 'request_type': request_type,
295
+ 'model': chat_request.model, 'status_code': 408})
296
+ raise
297
+ else:
298
+ raise
299
+ except Exception as e:
300
+ # 使用统一的API错误处理函数
301
+ error_result = await handle_api_error(
302
+ e,
303
+ current_api_key,
304
+ key_manager,
305
+ request_type,
306
+ chat_request.model,
307
+ server_retry - 1
308
+ )
309
+
310
+ # 如果需要删除缓存,清除缓存
311
+ if error_result.get('remove_cache', False) and cache_key and cache_key in response_cache_manager.cache:
312
+ log('info', f"因API错误,删除缓存: {cache_key[:8]}...",
313
+ extra={'cache_operation': 'remove-on-error', 'request_type': request_type})
314
+ del response_cache_manager.cache[cache_key]
315
+
316
+ if error_result.get('should_retry', False):
317
+ # 服务器错误需要重试(等待已在handle_api_error中完成)
318
+ continue
319
+ elif error_result.get('should_switch_key', False) and attempt < retry_attempts:
320
+ # 跳出服务器错误重试循环,获取下一个可用密钥
321
+ log('info', f"API密钥 {current_api_key[:8]}... 失败,准备尝试下一个密钥",
322
+ extra={'key': current_api_key[:8], 'request_type': request_type})
323
+ break
324
+ else:
325
+ # 无法处理的错误或已达到重试上限
326
+ break
327
+
328
+ # 如果所有尝试都失败
329
+ msg = "所有API密钥均请求失败,请稍后重试"
330
+ log('error', "API key 替换失败,所有API key都已尝试,请重新配置或稍后重试", extra={'key': 'N/A', 'request_type': 'switch_key', 'status_code': 'N/A'})
331
+
332
+ # 对于流式请求,创建一个特殊的StreamingResponse返回错误
333
+ if chat_request.stream:
334
+ async def error_generator():
335
+ error_json = json.dumps({'error': {'message': msg, 'type': 'api_error'}})
336
+ yield f"data: {error_json}\n\n"
337
+ yield "data: [DONE]\n\n"
338
+
339
+ return StreamingResponse(error_generator(), media_type="text/event-stream")
340
+ else:
341
+ # 非流式请求使用标准HTTP异常
342
+ raise HTTPException(
343
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=msg)
344
+
345
+ # 流式请求处理函数
346
+ async def process_stream_request(
347
+ chat_request: ChatCompletionRequest,
348
+ http_request: Request,
349
+ contents,
350
+ system_instruction,
351
+ current_api_key: str
352
+ ) -> StreamingResponse:
353
+ """处理流式API请求"""
354
+
355
+ # 创建一个直接流式响应的生成器函数
356
+ async def stream_response_generator():
357
+ # 如果启用了假流式模式,使用随机遍历API密钥的方式
358
+ if FAKE_STREAMING:
359
+ # 创建一个队列用于在任务之间传递数据
360
+ queue = asyncio.Queue()
361
+ keep_alive_task = None
362
+ api_request_task = None
363
+
364
+ try:
365
+ # 创建一个保持连接的任务,持续发送换行符
366
+ async def keep_alive_sender():
367
+ try:
368
+ # 创建一个Gemini客户端用于发送保持连接的换行符
369
+ keep_alive_client = GeminiClient(current_api_key)
370
+
371
+ # 启动保持连接的生成器
372
+ keep_alive_generator = keep_alive_client.stream_chat(
373
+ chat_request,
374
+ contents,
375
+ safety_settings_g2 if 'gemini-2.0-flash-exp' in chat_request.model else safety_settings,
376
+ system_instruction
377
+ )
378
+
379
+ # 持续发送换行符直到被取消
380
+ async for line in keep_alive_generator:
381
+ if line == "\n":
382
+ # 将换行符格式化为SSE格式
383
+ formatted_chunk = {
384
+ "id": "chatcmpl-keepalive",
385
+ "object": "chat.completion.chunk",
386
+ "created": int(time.time()),
387
+ "model": chat_request.model,
388
+ "choices": [{"delta": {"content": ""}, "index": 0, "finish_reason": None}]
389
+ }
390
+ # 将格式化的换行符放入队列
391
+ await queue.put(f"data: {json.dumps(formatted_chunk)}\n\n")
392
+ except asyncio.CancelledError:
393
+ log('info', "保持连接任务被取消",
394
+ extra={'key': current_api_key[:8], 'request_type': 'fake-stream'})
395
+ raise
396
+ except Exception as e:
397
+ log('error', f"保持连接任务出错: {str(e)}",
398
+ extra={'key': current_api_key[:8], 'request_type': 'fake-stream'})
399
+ # 将错误放入队列
400
+ await queue.put(None)
401
+ raise
402
+
403
+ # 创建一个任务来随机遍历API密钥并请求内容
404
+ async def api_request_handler():
405
+ success = False
406
+ try:
407
+ # 重置已尝试的密钥
408
+ key_manager.reset_tried_keys_for_request()
409
+
410
+ # 获取可用的API密钥
411
+ available_keys = key_manager.api_keys.copy()
412
+ random.shuffle(available_keys) # 随机打乱密钥顺序
413
+
414
+ # 遍历所有API密钥尝试获取响应
415
+ for attempt, api_key in enumerate(available_keys, 1):
416
+ try:
417
+ log('info', f"假流式模式: 尝试API密钥 {api_key[:8]}... ({attempt}/{len(available_keys)})",
418
+ extra={'key': api_key[:8], 'request_type': 'fake-stream', 'model': chat_request.model})
419
+
420
+ # 创建一个新的客户端使用当前API密钥
421
+ non_stream_client = GeminiClient(api_key)
422
+
423
+ # 使用非流式方式请求内容
424
+ response_content = await asyncio.to_thread(
425
+ non_stream_client.complete_chat,
426
+ chat_request,
427
+ contents,
428
+ safety_settings_g2 if 'gemini-2.0-flash-exp' in chat_request.model else safety_settings,
429
+ system_instruction
430
+ )
431
+
432
+ # 检查响应是否有效
433
+ if response_content and response_content.text:
434
+ log('info', f"假流式模式: API密钥 {api_key[:8]}... 成功获取响应",
435
+ extra={'key': api_key[:8], 'request_type': 'fake-stream', 'model': chat_request.model})
436
+
437
+ # 将完整响应分割成小块,模拟流式返回
438
+ full_text = response_content.text
439
+ chunk_size = max(len(full_text) // 10, 1) # 至少分成10块,每块至少1个字符
440
+
441
+ for i in range(0, len(full_text), chunk_size):
442
+ chunk = full_text[i:i+chunk_size]
443
+ formatted_chunk = {
444
+ "id": "chatcmpl-someid",
445
+ "object": "chat.completion.chunk",
446
+ "created": int(time.time()),
447
+ "model": chat_request.model,
448
+ "choices": [{"delta": {"role": "assistant", "content": chunk}, "index": 0, "finish_reason": None}]
449
+ }
450
+ # 将格式化的内容块放入队列
451
+ await queue.put(f"data: {json.dumps(formatted_chunk)}\n\n")
452
+
453
+ success = True
454
+ # 更新API调用统计
455
+ from app.utils.stats import update_api_call_stats
456
+ update_api_call_stats(api_call_stats,api_key)
457
+ break # 成功获取响应,退出循环
458
+ else:
459
+ log('warning', f"假流式模式: API密钥 {api_key[:8]}... 返回空响应",
460
+ extra={'key': api_key[:8], 'request_type': 'fake-stream', 'model': chat_request.model})
461
+ except Exception as e:
462
+ error_detail = handle_gemini_error(e, api_key, key_manager)
463
+ log('error', f"假流式模式: API密钥 {api_key[:8]}... 请求失败: {error_detail}",
464
+ extra={'key': api_key[:8], 'request_type': 'fake-stream', 'model': chat_request.model})
465
+ # 继续尝试下一个API密钥
466
+
467
+ # 如果所有API密钥都尝试失败
468
+ if not success:
469
+ error_msg = "所有API密钥均请求失败,请稍后重试"
470
+ log('error', error_msg,
471
+ extra={'key': 'ALL', 'request_type': 'fake-stream', 'model': chat_request.model})
472
+
473
+ # 添加错误信息到队列
474
+ error_json = {
475
+ "id": "chatcmpl-error",
476
+ "object": "chat.completion.chunk",
477
+ "created": int(time.time()),
478
+ "model": chat_request.model,
479
+ "choices": [{"delta": {"content": f"\n\n[错误: {error_msg}]"}, "index": 0, "finish_reason": "error"}]
480
+ }
481
+ await queue.put(f"data: {json.dumps(error_json)}\n\n")
482
+
483
+ # 添加完成标记到队列
484
+ await queue.put("data: [DONE]\n\n")
485
+ # 添加None表示队列结束
486
+ await queue.put(None)
487
+
488
+ except asyncio.CancelledError:
489
+ log('info', "API请求任务被取消",
490
+ extra={'key': current_api_key[:8], 'request_type': 'fake-stream'})
491
+ # 添加None表示队列结束
492
+ await queue.put(None)
493
+ raise
494
+ except Exception as e:
495
+ log('error', f"API请求任务出错: {str(e)}",
496
+ extra={'key': current_api_key[:8], 'request_type': 'fake-stream'})
497
+ # 添加错误信息到队列
498
+ error_json = {
499
+ "id": "chatcmpl-error",
500
+ "object": "chat.completion.chunk",
501
+ "created": int(time.time()),
502
+ "model": chat_request.model,
503
+ "choices": [{"delta": {"content": f"\n\n[错误: {str(e)}]"}, "index": 0, "finish_reason": "error"}]
504
+ }
505
+ await queue.put(f"data: {json.dumps(error_json)}\n\n")
506
+ await queue.put("data: [DONE]\n\n")
507
+ # 添加None表示队列结束
508
+ await queue.put(None)
509
+ raise
510
+
511
+ # 启动保持连接的任务
512
+ keep_alive_task = asyncio.create_task(keep_alive_sender())
513
+ # 启动API请求任务
514
+ api_request_task = asyncio.create_task(api_request_handler())
515
+
516
+ # 从队列中获取数据并发送给客户端
517
+ while True:
518
+ chunk = await queue.get()
519
+ if chunk is None: # None表示队列结束
520
+ break
521
+ yield chunk
522
+
523
+ # 如果API请求任务已完成,取消保持连接任务
524
+ if api_request_task.done() and not keep_alive_task.done():
525
+ keep_alive_task.cancel()
526
+
527
+ except asyncio.CancelledError:
528
+ log('info', "流式响应生成器被取消",
529
+ extra={'key': current_api_key[:8], 'request_type': 'fake-stream'})
530
+ # 取消所有任务
531
+ if keep_alive_task and not keep_alive_task.done():
532
+ keep_alive_task.cancel()
533
+ if api_request_task and not api_request_task.done():
534
+ api_request_task.cancel()
535
+ except Exception as e:
536
+ log('error', f"流式响应生成器出错: {str(e)}",
537
+ extra={'key': current_api_key[:8], 'request_type': 'fake-stream'})
538
+ # 取消所有任务
539
+ if keep_alive_task and not keep_alive_task.done():
540
+ keep_alive_task.cancel()
541
+ if api_request_task and not api_request_task.done():
542
+ api_request_task.cancel()
543
+ # 发送错误信息给客户端
544
+ error_json = {
545
+ "id": "chatcmpl-error",
546
+ "object": "chat.completion.chunk",
547
+ "created": int(time.time()),
548
+ "model": chat_request.model,
549
+ "choices": [{"delta": {"content": f"\n\n[错误: {str(e)}]"}, "index": 0, "finish_reason": "error"}]
550
+ }
551
+ yield f"data: {json.dumps(error_json)}\n\n"
552
+ yield "data: [DONE]\n\n"
553
+ finally:
554
+ # 确保所有任务都被取消
555
+ if keep_alive_task and not keep_alive_task.done():
556
+ keep_alive_task.cancel()
557
+ if api_request_task and not api_request_task.done():
558
+ api_request_task.cancel()
559
+ else:
560
+ # 原始流式请求处理逻辑
561
+ gemini_client = GeminiClient(current_api_key)
562
+ success = False
563
+
564
+ try:
565
+ # 直接迭代生成器并发送响应块
566
+ async for chunk in gemini_client.stream_chat(
567
+ chat_request,
568
+ contents,
569
+ safety_settings_g2 if 'gemini-2.0-flash-exp' in chat_request.model else safety_settings,
570
+ system_instruction
571
+ ):
572
+ # 空字符串跳过
573
+ if not chunk:
574
+ continue
575
+
576
+ formatted_chunk = {
577
+ "id": "chatcmpl-someid",
578
+ "object": "chat.completion.chunk",
579
+ "created": int(time.time()),
580
+ "model": chat_request.model,
581
+ "choices": [{"delta": {"role": "assistant", "content": chunk}, "index": 0, "finish_reason": None}]
582
+ }
583
+ success = True # 只要有一个chunk成功,就标记为成功
584
+ yield f"data: {json.dumps(formatted_chunk)}\n\n"
585
+
586
+ # 如果成功获取到响应,更新API调用统计
587
+ if success:
588
+ from app.utils.stats import update_api_call_stats
589
+ update_api_call_stats(api_call_stats, current_api_key)
590
+
591
+ yield "data: [DONE]\n\n"
592
+
593
+ except asyncio.CancelledError:
594
+ extra_log_cancel = {'key': current_api_key[:8], 'request_type': 'stream', 'model': chat_request.model, 'error_message': '客户端已断开连接'}
595
+ log('info', "客户端连接已中断", extra=extra_log_cancel)
596
+ except Exception as e:
597
+ error_detail = handle_gemini_error(e, current_api_key, key_manager)
598
+ log('error', f"流式请求失败: {error_detail}",
599
+ extra={'key': current_api_key[:8], 'request_type': 'stream', 'model': chat_request.model})
600
+ # 发送错误信息给客户端
601
+ error_json = {
602
+ "id": "chatcmpl-error",
603
+ "object": "chat.completion.chunk",
604
+ "created": int(time.time()),
605
+ "model": chat_request.model,
606
+ "choices": [{"delta": {"content": f"\n\n[错误: {error_detail}]"}, "index": 0, "finish_reason": "error"}]
607
+ }
608
+ yield f"data: {json.dumps(error_json)}\n\n"
609
+ yield "data: [DONE]\n\n"
610
+ # 重新抛出异常,这样process_request可以捕获它
611
+ raise e
612
+
613
+ return StreamingResponse(stream_response_generator(), media_type="text/event-stream")
614
+
615
+ # Gemini完成请求函数
616
+ async def run_gemini_completion(
617
+ gemini_client,
618
+ chat_request: ChatCompletionRequest,
619
+ contents,
620
+ system_instruction,
621
+ request_type: str,
622
+ current_api_key: str
623
+ ):
624
+ """运行Gemini非流式请求"""
625
+ # 记录函数调用状态
626
+ run_fn = run_gemini_completion
627
+
628
+ try:
629
+ # 创建一个不会被客户端断开影响的任务
630
+ response_future = asyncio.create_task(
631
+ asyncio.to_thread(
632
+ gemini_client.complete_chat,
633
+ chat_request,
634
+ contents,
635
+ safety_settings_g2 if 'gemini-2.0-flash-exp' in chat_request.model else safety_settings,
636
+ system_instruction
637
+ )
638
+ )
639
+
640
+ # 使用shield防止任务被外部取消
641
+ response_content = await asyncio.shield(response_future)
642
+
643
+ # 只在第一次调用时记录完成日志
644
+ if not hasattr(run_fn, 'logged_complete'):
645
+ log('info', "非流式请求成功完成", extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
646
+ run_fn.logged_complete = True
647
+ return response_content
648
+ except asyncio.CancelledError:
649
+ # 即使任务被取消,我们也确保正在进行的API请求能够完成
650
+ if 'response_future' in locals() and not response_future.done():
651
+ try:
652
+ # 使用shield确保任务不被取消,并等待它完成
653
+ response_content = await asyncio.shield(response_future)
654
+ log('info', "API请求在客户端断开后完成", extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
655
+ return response_content
656
+ except Exception as e:
657
+ extra_log_gemini_cancel = {'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model, 'error_message': f'API请求在客户端断开后失败: {str(e)}'}
658
+ log('info', "API调用因客户端断开而失败", extra=extra_log_gemini_cancel)
659
+ raise
660
+
661
+ # 如果任务尚未开始或已经失败,记录日志
662
+ extra_log_gemini_cancel = {'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model, 'error_message': '客户端断开导致API调用取消'}
663
+ log('info', "API调用因客户端断开而取消", extra=extra_log_gemini_cancel)
664
+ raise
665
+
666
+ # 客户端断开检测函数
667
+ async def check_client_disconnect(http_request: Request, current_api_key: str, request_type: str, model: str):
668
+ """检查客户端是否断开连接"""
669
+ while True:
670
+ if await http_request.is_disconnected():
671
+ extra_log = {'key': current_api_key[:8], 'request_type': request_type, 'model': model, 'error_message': '检测到客户端断开连接'}
672
+ log('info', "客户端连接已中断,等待API请求完成", extra=extra_log)
673
+ return True
674
+ await asyncio.sleep(0.5)
675
+
676
+ # 客户端断开处理函数
677
+ async def handle_client_disconnect(
678
+ gemini_task: asyncio.Task,
679
+ chat_request: ChatCompletionRequest,
680
+ request_type: str,
681
+ current_api_key: str,
682
+ cache_key: str = None,
683
+ client_ip: str = None
684
+ ):
685
+ try:
686
+ # 等待API任务完成,使用shield防止它被取消
687
+ response_content = await asyncio.shield(gemini_task)
688
+
689
+ # 检查响应文本是否为空
690
+ if response_content is None or response_content.text == "":
691
+ if response_content is None:
692
+ log('info', "客户端断开后API任务返回None",
693
+ extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
694
+ else:
695
+ extra_log = {'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model, 'status_code': 204}
696
+ log('info', "客户端断开后Gemini API 返���空响应", extra=extra_log)
697
+
698
+ # 删除任何现有缓存,因为响应为空
699
+ if cache_key and cache_key in response_cache_manager.cache:
700
+ log('info', f"因空响应,删除缓存: {cache_key[:8]}...",
701
+ extra={'cache_operation': 'remove-on-empty', 'request_type': request_type})
702
+ del response_cache_manager.cache[cache_key]
703
+
704
+ # 返回错误响应而不是None
705
+ return create_error_response(chat_request.model, "AI未返回任何内容,请重试")
706
+
707
+ # 首先检查是否有现有缓存
708
+ cached_response, cache_hit = response_cache_manager.get(cache_key)
709
+ if cache_hit:
710
+ log('info', f"客户端断开但找到已存在缓存,将删除: {cache_key[:8]}...",
711
+ extra={'cache_operation': 'disconnect-found-cache', 'request_type': request_type})
712
+
713
+ # 安全删除缓存
714
+ if cache_key in response_cache_manager.cache:
715
+ del response_cache_manager.cache[cache_key]
716
+
717
+ # 不返回缓存,而是创建新响应并缓存
718
+
719
+ # 创建新响应
720
+ from app.utils.response import create_response
721
+ response = create_response(chat_request, response_content)
722
+
723
+ # 客户端已断开,此响应不会实际发送,可以考虑将其缓存以供后续使用
724
+ # 如果确实需要缓存,则可以取消下面的注释
725
+ # cache_response(response, cache_key, client_ip)
726
+
727
+ return response
728
+ except asyncio.CancelledError:
729
+ # 对于取消异常,仍然尝试继续完成任务
730
+ log('info', "客户端断开后任务被取消,但我们仍会尝试完成",
731
+ extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
732
+
733
+ # 检查任务是否已经完成
734
+ if gemini_task.done() and not gemini_task.cancelled():
735
+ try:
736
+ response_content = gemini_task.result()
737
+
738
+ # 首先检查是否有现有缓存
739
+ cached_response, cache_hit = response_cache_manager.get(cache_key)
740
+ if cache_hit:
741
+ log('info', f"任务被取消但找到已存在缓存,将删除: {cache_key[:8]}...",
742
+ extra={'cache_operation': 'cancel-found-cache', 'request_type': request_type})
743
+
744
+ # 安全删除缓存
745
+ if cache_key in response_cache_manager.cache:
746
+ del response_cache_manager.cache[cache_key]
747
+
748
+ # 创建但不缓存响应
749
+ from app.utils.response import create_response
750
+ response = create_response(chat_request, response_content)
751
+ return response
752
+ except Exception as inner_e:
753
+ log('error', f"客户端断开后从已完成任务获取结果失败: {str(inner_e)}",
754
+ extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
755
+
756
+ # 删除缓存,因为出现错误
757
+ if cache_key and cache_key in response_cache_manager.cache:
758
+ log('info', f"因任务获取结果失败,删除缓存: {cache_key[:8]}...",
759
+ extra={'cache_operation': 'remove-on-error', 'request_type': request_type})
760
+ del response_cache_manager.cache[cache_key]
761
+
762
+ # 创建错误响应而不是返回None
763
+ return create_error_response(chat_request.model, "请求处理过程中发生错误,请重试")
764
+ except Exception as e:
765
+ # 处理API任务异常
766
+ error_msg = str(e)
767
+ extra_log = {'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model, 'error_message': error_msg}
768
+ log('error', f"客户端断开后处理API响应时出错: {error_msg}", extra=extra_log)
769
+
770
+ # 删除缓存,因为出现错误
771
+ if cache_key and cache_key in response_cache_manager.cache:
772
+ log('info', f"因API响应错误,删除缓存: {cache_key[:8]}...",
773
+ extra={'cache_operation': 'remove-on-error', 'request_type': request_type})
774
+ del response_cache_manager.cache[cache_key]
775
+
776
+ # 创建错误响应而不是返回None
777
+ return create_error_response(chat_request.model, f"请求处理错误: {error_msg}")
778
+
779
+ # 非流式请求处理函数
780
+ async def process_nonstream_request(
781
+ chat_request: ChatCompletionRequest,
782
+ http_request: Request,
783
+ request_type: str,
784
+ contents,
785
+ system_instruction,
786
+ current_api_key: str,
787
+ cache_key: str = None,
788
+ client_ip: str = None
789
+ ):
790
+ """处理非流式API请求"""
791
+ gemini_client = GeminiClient(current_api_key)
792
+
793
+ # 创建任务
794
+ gemini_task = asyncio.create_task(
795
+ run_gemini_completion(
796
+ gemini_client,
797
+ chat_request,
798
+ contents,
799
+ system_instruction,
800
+ request_type,
801
+ current_api_key
802
+ )
803
+ )
804
+
805
+ disconnect_task = asyncio.create_task(
806
+ check_client_disconnect(
807
+ http_request,
808
+ current_api_key,
809
+ request_type,
810
+ chat_request.model
811
+ )
812
+ )
813
+
814
+ try:
815
+ # 先等待看是否API任务先完成,或者客户端先断开连接
816
+ done, pending = await asyncio.wait(
817
+ [gemini_task, disconnect_task],
818
+ return_when=asyncio.FIRST_COMPLETED
819
+ )
820
+
821
+ if disconnect_task in done:
822
+ # 客户端已断开连接,但我们仍继续完成API请求以便缓存结果
823
+ return await handle_client_disconnect(
824
+ gemini_task,
825
+ chat_request,
826
+ request_type,
827
+ current_api_key,
828
+ cache_key,
829
+ client_ip
830
+ )
831
+ else:
832
+ # API任务先完成,取消断开检测任务
833
+ disconnect_task.cancel()
834
+
835
+ # 获取响应内容
836
+ response_content = await gemini_task
837
+
838
+ # 检查缓存是否已经存在,如果存在则不再创建新缓存
839
+ cached_response, cache_hit = response_cache_manager.get(cache_key)
840
+ if cache_hit:
841
+ log('info', f"缓存已存在,直接返回: {cache_key[:8]}...",
842
+ extra={'cache_operation': 'use-existing', 'request_type': request_type})
843
+
844
+ # 安全删除缓存
845
+ if cache_key in response_cache_manager.cache:
846
+ del response_cache_manager.cache[cache_key]
847
+ log('info', f"缓存使用后已删除: {cache_key[:8]}...",
848
+ extra={'cache_operation': 'used-and-removed', 'request_type': request_type})
849
+
850
+ return cached_response
851
+
852
+ # 创建响应
853
+ from app.utils.response import create_response
854
+ response = create_response(chat_request, response_content)
855
+
856
+ # 缓存响应
857
+ cache_response(response, cache_key, client_ip, response_cache_manager, update_api_call_stats, api_key=current_api_key)
858
+
859
+ # 立即删除缓存,确保只能使用一次
860
+ if cache_key and cache_key in response_cache_manager.cache:
861
+ del response_cache_manager.cache[cache_key]
862
+ log('info', f"缓存创建后立即删除: {cache_key[:8]}...",
863
+ extra={'cache_operation': 'store-and-remove', 'request_type': request_type})
864
+
865
+ # 返回响应
866
+ return response
867
+
868
+ except asyncio.CancelledError:
869
+ extra_log = {'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model, 'error_message':"请求被取消"}
870
+ log('info', "请求取消", extra=extra_log)
871
+
872
+ # 在请求被取消时先检查缓存中是否已有结果
873
+ cached_response, cache_hit = response_cache_manager.get(cache_key)
874
+ if cache_hit:
875
+ log('info', f"请求取消但找到有效缓存,使用缓存响应: {cache_key[:8]}...",
876
+ extra={'cache_operation': 'use-cache-on-cancel', 'request_type': request_type})
877
+
878
+ # 安全删除缓存
879
+ if cache_key in response_cache_manager.cache:
880
+ del response_cache_manager.cache[cache_key]
881
+ log('info', f"缓存使用后已删除: {cache_key[:8]}...",
882
+ extra={'cache_operation': 'used-and-removed', 'request_type': request_type})
883
+
884
+ return cached_response
885
+
886
+ # 尝试完成正在进行的API请求
887
+ if not gemini_task.done():
888
+ log('info', "请求取消但API请求尚未完成,继续等待...",
889
+ extra={'key': current_api_key[:8], 'request_type': request_type})
890
+
891
+ # 使用shield确保任务不会被取消
892
+ response_content = await asyncio.shield(gemini_task)
893
+
894
+ # 创建响应
895
+ from app.utils.response import create_response
896
+ response = create_response(chat_request, response_content)
897
+
898
+ # 不缓存这个响应,直接返回
899
+ return response
900
+ else:
901
+ # 任务已完成,获取结果
902
+ response_content = gemini_task.result()
903
+
904
+ # 创建响应
905
+ from app.utils.response import create_response
906
+ response = create_response(chat_request, response_content)
907
+
908
+ # 不缓存这个响应,直接返回
909
+ return response
910
+
911
+ except HTTPException as e:
912
+ if e.status_code == status.HTTP_408_REQUEST_TIMEOUT:
913
+ extra_log = {'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model,
914
+ 'status_code': 408, 'error_message': '客户端连接中断'}
915
+ log('error', "客户端连接中断,终止后续重试", extra=extra_log)
916
+ raise
917
+ else:
918
+ raise
hajimi.v0.0.4/app/config/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # 配置模块初始化文件
2
+ from app.config.settings import *
3
+ from app.config.safety import *
hajimi.v0.0.4/app/config/safety.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 安全设置配置
2
+
3
+ # Gemini 1.0 安全设置
4
+ SAFETY_SETTINGS = [
5
+ {
6
+ "category": "HARM_CATEGORY_HARASSMENT",
7
+ "threshold": "BLOCK_NONE"
8
+ },
9
+ {
10
+ "category": "HARM_CATEGORY_HATE_SPEECH",
11
+ "threshold": "BLOCK_NONE"
12
+ },
13
+ {
14
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
15
+ "threshold": "BLOCK_NONE"
16
+ },
17
+ {
18
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
19
+ "threshold": "BLOCK_NONE"
20
+ },
21
+ {
22
+ "category": 'HARM_CATEGORY_CIVIC_INTEGRITY',
23
+ "threshold": 'BLOCK_NONE'
24
+ }
25
+ ]
26
+
27
+ # Gemini 2.0 安全设置
28
+ SAFETY_SETTINGS_G2 = [
29
+ {
30
+ "category": "HARM_CATEGORY_HARASSMENT",
31
+ "threshold": "OFF"
32
+ },
33
+ {
34
+ "category": "HARM_CATEGORY_HATE_SPEECH",
35
+ "threshold": "OFF"
36
+ },
37
+ {
38
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
39
+ "threshold": "OFF"
40
+ },
41
+ {
42
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
43
+ "threshold": "OFF"
44
+ },
45
+ {
46
+ "category": 'HARM_CATEGORY_CIVIC_INTEGRITY',
47
+ "threshold": 'OFF'
48
+ }
49
+ ]
hajimi.v0.0.4/app/config/settings.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pathlib
3
+ import logging
4
+ from datetime import datetime, timedelta
5
+
6
+ # 基础目录设置
7
+ BASE_DIR = pathlib.Path(__file__).parent.parent
8
+
9
+ # 流式响应配置
10
+ FAKE_STREAMING = os.environ.get("FAKE_STREAMING", "true").lower() in ["true", "1", "yes"]
11
+ # 假流式请求的空内容返回间隔(秒)
12
+ FAKE_STREAMING_INTERVAL = float(os.environ.get("FAKE_STREAMING_INTERVAL", "1"))
13
+
14
+ # 日志配置
15
+ logging.getLogger("uvicorn").disabled = True
16
+ logging.getLogger("uvicorn.access").disabled = True
17
+
18
+ # 安全配置
19
+ PASSWORD = os.environ.get("PASSWORD", "123").strip('"')
20
+ MAX_REQUESTS_PER_MINUTE = int(os.environ.get("MAX_REQUESTS_PER_MINUTE", "30"))
21
+ MAX_REQUESTS_PER_DAY_PER_IP = int(os.environ.get("MAX_REQUESTS_PER_DAY_PER_IP", "600"))
22
+ RETRY_DELAY = 1
23
+ MAX_RETRY_DELAY = 16
24
+
25
+ # API密钥使用限制
26
+ # 默认每个API密钥每24小时可使用次数
27
+ API_KEY_DAILY_LIMIT = int(os.environ.get("API_KEY_DAILY_LIMIT", "25"))
28
+
29
+ # 缓存配置
30
+ CACHE_EXPIRY_TIME = int(os.environ.get("CACHE_EXPIRY_TIME", "1200")) # 默认20分钟
31
+ MAX_CACHE_ENTRIES = int(os.environ.get("MAX_CACHE_ENTRIES", "500")) # 默认最多缓存500条响应
32
+ REMOVE_CACHE_AFTER_USE = os.environ.get("REMOVE_CACHE_AFTER_USE", "true").lower() in ["true", "1", "yes"]
33
+
34
+ # 请求历史配置
35
+ REQUEST_HISTORY_EXPIRY_TIME = int(os.environ.get("REQUEST_HISTORY_EXPIRY_TIME", "600")) # 默认10分钟
36
+ ENABLE_RECONNECT_DETECTION = os.environ.get("ENABLE_RECONNECT_DETECTION", "true").lower() in ["true", "1", "yes"]
37
+
38
+ # 版本信息
39
+ local_version = "0.0.4"
40
+ remote_version = "0.0.4"
41
+ has_update = False
42
+
43
+ # API调用统计
44
+ api_call_stats = {
45
+ 'last_24h': {
46
+ 'total': {}, # 按小时统计过去24小时总调用次数
47
+ 'by_endpoint': {} # 按API端点分类的24小时统计(也用于API密钥统计)
48
+ },
49
+ 'hourly': {
50
+ 'total': {}, # 按小时统计过去一小时总调用次数
51
+ 'by_endpoint': {} # 按API端点分类的小时统计(也用于API密钥统计)
52
+ },
53
+ 'minute': {
54
+ 'total': {}, # 按分钟统计过去一分钟总调用次数
55
+ 'by_endpoint': {} # 按API端点分类的分钟统计(也用于API密钥统计)
56
+ }
57
+ }
58
+
59
+ # 客户端IP到最近请求的映射,用于识别重连请求
60
+ client_request_history = {}
61
+
62
+ # 模型屏蔽列表配置
63
+ # 默认屏蔽的模型列表
64
+ DEFAULT_BLOCKED_MODELS = []
65
+
66
+ # 从环境变量中读取屏蔽模型列表,如果未设置则使用默认列表
67
+ # 环境变量格式应为逗号分隔的模型名称字符串
68
+ BLOCKED_MODELS = os.environ.get("BLOCKED_MODELS", ",".join(DEFAULT_BLOCKED_MODELS))
69
+ # 将字符串转换为列表
70
+ BLOCKED_MODELS = [model.strip() for model in BLOCKED_MODELS.split(",") if model.strip()]
hajimi.v0.0.4/app/main.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Request, status
2
+ from fastapi.responses import JSONResponse, HTMLResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.templating import Jinja2Templates
5
+ from app.models import ErrorResponse
6
+ from app.services import GeminiClient
7
+ from app.utils import (
8
+ APIKeyManager,
9
+ test_api_key,
10
+ format_log_message,
11
+ log_manager,
12
+ ResponseCacheManager,
13
+ ActiveRequestsManager,
14
+ clean_expired_stats,
15
+ update_api_call_stats,
16
+ check_version,
17
+ schedule_cache_cleanup,
18
+ handle_exception,
19
+ log
20
+ )
21
+ from app.api import router, init_router, dashboard_router, init_dashboard_router
22
+ from app.config.settings import (
23
+ FAKE_STREAMING,
24
+ FAKE_STREAMING_INTERVAL,
25
+ PASSWORD,
26
+ MAX_REQUESTS_PER_MINUTE,
27
+ MAX_REQUESTS_PER_DAY_PER_IP,
28
+ RETRY_DELAY,
29
+ MAX_RETRY_DELAY,
30
+ CACHE_EXPIRY_TIME,
31
+ MAX_CACHE_ENTRIES,
32
+ REMOVE_CACHE_AFTER_USE,
33
+ REQUEST_HISTORY_EXPIRY_TIME,
34
+ ENABLE_RECONNECT_DETECTION,
35
+ api_call_stats,
36
+ client_request_history,
37
+ local_version,
38
+ remote_version,
39
+ has_update,
40
+ API_KEY_DAILY_LIMIT
41
+ )
42
+ from app.config.safety import SAFETY_SETTINGS, SAFETY_SETTINGS_G2
43
+ import os
44
+ import json
45
+ import asyncio
46
+ import time
47
+ import logging
48
+ from datetime import datetime, timedelta
49
+ import sys
50
+ import pathlib
51
+
52
+ # 设置模板目录
53
+ BASE_DIR = pathlib.Path(__file__).parent
54
+ templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
55
+
56
+ app = FastAPI()
57
+
58
+ # --------------- 全局实例 ---------------
59
+
60
+ # 初始化API密钥管理器
61
+ key_manager = APIKeyManager()
62
+ current_api_key = key_manager.get_available_key()
63
+
64
+ # 创建全局缓存字典,将作为缓存管理器的内部存储
65
+ response_cache = {}
66
+
67
+ # 初始化缓存管理器,使用全局字典作为存储
68
+ response_cache_manager = ResponseCacheManager(
69
+ expiry_time=CACHE_EXPIRY_TIME,
70
+ max_entries=MAX_CACHE_ENTRIES,
71
+ remove_after_use=REMOVE_CACHE_AFTER_USE,
72
+ cache_dict=response_cache
73
+ )
74
+
75
+ # 活跃请求池 - 将作为活跃请求管理器的内部存储
76
+ active_requests_pool = {}
77
+
78
+ # 初始化活跃请求管理器
79
+ active_requests_manager = ActiveRequestsManager(requests_pool=active_requests_pool)
80
+
81
+ # --------------- 工具函数 ---------------
82
+
83
+ def switch_api_key():
84
+ global current_api_key
85
+ key = key_manager.get_available_key() # get_available_key 会处理栈的逻辑
86
+ if key:
87
+ current_api_key = key
88
+ log('info', f"API key 替换为 → {current_api_key[:8]}...", extra={'key': current_api_key[:8], 'request_type': 'switch_key'})
89
+ else:
90
+ log('error', "API key 替换失败,所有API key都已尝试,请重新配置或稍后重试", extra={'key': 'N/A', 'request_type': 'switch_key', 'status_code': 'N/A'})
91
+
92
+ async def check_keys():
93
+ available_keys = []
94
+ for key in key_manager.api_keys:
95
+ is_valid = await test_api_key(key)
96
+ status_msg = "有效" if is_valid else "无效"
97
+ log('info', f"API Key {key[:10]}... {status_msg}.")
98
+ if is_valid:
99
+ available_keys.append(key)
100
+ if not available_keys:
101
+ log('error', "没有可用的 API 密钥!", extra={'key': 'N/A', 'request_type': 'startup', 'status_code': 'N/A'})
102
+ return available_keys
103
+
104
+ # 设置全局异常处理
105
+ sys.excepthook = handle_exception
106
+
107
+ # --------------- 事件处理 ---------------
108
+
109
+ @app.on_event("startup")
110
+ async def startup_event():
111
+ log('info', "Starting Gemini API proxy...")
112
+
113
+ # 启动缓存清理定时任务
114
+ schedule_cache_cleanup(response_cache_manager, active_requests_manager)
115
+
116
+ # 检查版本
117
+ await check_version()
118
+
119
+ available_keys = await check_keys()
120
+ if available_keys:
121
+ key_manager.api_keys = available_keys
122
+ key_manager._reset_key_stack() # 启动时也确保创建随机栈
123
+ key_manager.show_all_keys()
124
+ log('info', f"可用 API 密钥数量:{len(key_manager.api_keys)}")
125
+ log('info', f"最大重试次数设置为:{len(key_manager.api_keys)}")
126
+ if key_manager.api_keys:
127
+ all_models = await GeminiClient.list_available_models(key_manager.api_keys[0])
128
+ GeminiClient.AVAILABLE_MODELS = [model.replace(
129
+ "models/", "") for model in all_models]
130
+ log('info', "Available models loaded.")
131
+
132
+ # 初始化路由器
133
+ init_router(
134
+ key_manager,
135
+ response_cache_manager,
136
+ active_requests_manager,
137
+ SAFETY_SETTINGS,
138
+ SAFETY_SETTINGS_G2,
139
+ current_api_key,
140
+ FAKE_STREAMING,
141
+ FAKE_STREAMING_INTERVAL,
142
+ PASSWORD,
143
+ MAX_REQUESTS_PER_MINUTE,
144
+ MAX_REQUESTS_PER_DAY_PER_IP
145
+ )
146
+
147
+ # 初始化仪表盘路由器
148
+ init_dashboard_router(
149
+ key_manager,
150
+ response_cache_manager,
151
+ active_requests_manager
152
+ )
153
+
154
+ # --------------- 异常处理 ---------------
155
+
156
+ @app.exception_handler(Exception)
157
+ async def global_exception_handler(request: Request, exc: Exception):
158
+ from app.utils import translate_error
159
+ error_message = translate_error(str(exc))
160
+ extra_log_unhandled_exception = {'status_code': 500, 'error_message': error_message}
161
+ log('error', f"Unhandled exception: {error_message}", extra=extra_log_unhandled_exception)
162
+ return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ErrorResponse(message=str(exc), type="internal_error").dict())
163
+
164
+ # --------------- 路由 ---------------
165
+
166
+ # 包含API路由
167
+ app.include_router(router)
168
+ app.include_router(dashboard_router)
169
+
170
+ @app.get("/", response_class=HTMLResponse)
171
+ async def root(request: Request):
172
+ # 先清理过期数据,确保统计数据是最新的
173
+ clean_expired_stats(api_call_stats)
174
+ response_cache_manager.clean_expired() # 使用管理器清理缓存
175
+ active_requests_manager.clean_completed() # 使用管理器清理活跃请求
176
+ # 获取当前统计数据
177
+ now = datetime.now()
178
+
179
+ # 计算过去24小时的调用总数
180
+ last_24h_calls = sum(api_call_stats['last_24h']['total'].values())
181
+
182
+ # 计算过去一小时内的调用总数
183
+ one_hour_ago = now - timedelta(hours=1)
184
+ hourly_calls = 0
185
+ for hour_key, count in api_call_stats['hourly']['total'].items():
186
+ try:
187
+ hour_time = datetime.strptime(hour_key, '%Y-%m-%d %H:00')
188
+ if hour_time >= one_hour_ago:
189
+ hourly_calls += count
190
+ except ValueError:
191
+ continue
192
+
193
+ # 计算过去一分钟内的调用总数
194
+ one_minute_ago = now - timedelta(minutes=1)
195
+ minute_calls = 0
196
+ for minute_key, count in api_call_stats['minute']['total'].items():
197
+ try:
198
+ minute_time = datetime.strptime(minute_key, '%Y-%m-%d %H:%M')
199
+ if minute_time >= one_minute_ago:
200
+ minute_calls += count
201
+ except ValueError:
202
+ continue
203
+
204
+ # 获取最近的日志
205
+ recent_logs = log_manager.get_recent_logs(50) # 获取最近50条日志
206
+
207
+ # 获取缓存统计
208
+ total_cache = len(response_cache_manager.cache)
209
+ valid_cache = sum(1 for _, data in response_cache_manager.cache.items()
210
+ if time.time() < data.get('expiry_time', 0))
211
+ cache_by_model = {}
212
+
213
+ # 分析缓存数据
214
+ for _, cache_data in response_cache_manager.cache.items():
215
+ if time.time() < cache_data.get('expiry_time', 0):
216
+ # 按模型统计缓存
217
+ model = cache_data.get('response', {}).model
218
+ if model:
219
+ if model in cache_by_model:
220
+ cache_by_model[model] += 1
221
+ else:
222
+ cache_by_model[model] = 1
223
+
224
+ # 获取请求历史统计
225
+ history_count = len(client_request_history)
226
+
227
+ # 获取活跃请求统计
228
+ active_count = len(active_requests_manager.active_requests)
229
+ active_done = sum(1 for task in active_requests_manager.active_requests.values() if task.done())
230
+ active_pending = active_count - active_done
231
+
232
+ # 获取API密钥使用统计
233
+ api_key_stats = []
234
+ for api_key in key_manager.api_keys:
235
+ # 获取API密钥前8位作为标识
236
+ api_key_id = api_key[:8]
237
+
238
+ # 计算24小时内的调用次数
239
+ calls_24h = 0
240
+ if 'by_endpoint' in api_call_stats['last_24h'] and api_key in api_call_stats['last_24h']['by_endpoint']:
241
+ calls_24h = sum(api_call_stats['last_24h']['by_endpoint'][api_key].values())
242
+
243
+ # 计算使用百分比
244
+ usage_percent = (calls_24h / API_KEY_DAILY_LIMIT) * 100 if API_KEY_DAILY_LIMIT > 0 else 0
245
+
246
+ # 添加到结果列表
247
+ api_key_stats.append({
248
+ 'api_key': api_key_id,
249
+ 'calls_24h': calls_24h,
250
+ 'limit': API_KEY_DAILY_LIMIT,
251
+ 'usage_percent': round(usage_percent, 2)
252
+ })
253
+
254
+ # 按使用百分比降序排序
255
+ api_key_stats.sort(key=lambda x: x['usage_percent'], reverse=True)
256
+
257
+ # 准备模板上下文
258
+ context = {
259
+ "key_count": len(key_manager.api_keys),
260
+ "model_count": len(GeminiClient.AVAILABLE_MODELS),
261
+ "retry_count": len(key_manager.api_keys),
262
+ "last_24h_calls": last_24h_calls,
263
+ "hourly_calls": hourly_calls,
264
+ "minute_calls": minute_calls,
265
+ "max_requests_per_minute": MAX_REQUESTS_PER_MINUTE,
266
+ "max_requests_per_day_per_ip": MAX_REQUESTS_PER_DAY_PER_IP,
267
+ "current_time": datetime.now().strftime('%H:%M:%S'),
268
+ "logs": recent_logs,
269
+ # 添加版本信息
270
+ "local_version": local_version,
271
+ "remote_version": remote_version,
272
+ "has_update": has_update,
273
+ # 添加缓存信息
274
+ "cache_entries": total_cache,
275
+ "valid_cache": valid_cache,
276
+ "expired_cache": total_cache - valid_cache,
277
+ "cache_expiry_time": CACHE_EXPIRY_TIME,
278
+ "max_cache_entries": MAX_CACHE_ENTRIES,
279
+ "cache_by_model": cache_by_model,
280
+ "request_history_count": history_count,
281
+ "enable_reconnect_detection": ENABLE_RECONNECT_DETECTION,
282
+ "remove_cache_after_use": REMOVE_CACHE_AFTER_USE,
283
+ # 添加活跃请求池信息
284
+ "active_count": active_count,
285
+ "active_done": active_done,
286
+ "active_pending": active_pending,
287
+ # 添加API密钥统计
288
+ "api_key_stats": api_key_stats,
289
+ }
290
+
291
+ # 使用Jinja2模板引擎正确渲染HTML
292
+ return templates.TemplateResponse("index.html", {"request": request, **context})
hajimi.v0.0.4/app/models/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.models.schemas import (
2
+ Message,
3
+ ChatCompletionRequest,
4
+ Choice,
5
+ Usage,
6
+ ChatCompletionResponse,
7
+ ErrorResponse,
8
+ ModelList
9
+ )
10
+
11
+ __all__ = [
12
+ 'Message',
13
+ 'ChatCompletionRequest',
14
+ 'Choice',
15
+ 'Usage',
16
+ 'ChatCompletionResponse',
17
+ 'ErrorResponse',
18
+ 'ModelList'
19
+ ]
hajimi.v0.0.4/app/models/schemas.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Optional, Union, Literal
2
+ from pydantic import BaseModel, Field
3
+
4
+ class Message(BaseModel):
5
+ role: str
6
+ content: Union[str, List[Dict[str, str]]]
7
+
8
+ class ChatCompletionRequest(BaseModel):
9
+ model: str
10
+ messages: List[Message]
11
+ temperature: float = 0.7
12
+ top_p: Optional[float] = 1.0
13
+ n: int = 1
14
+ stream: bool = False
15
+ stop: Optional[Union[str, List[str]]] = None
16
+ max_tokens: Optional[int] = None
17
+ presence_penalty: Optional[float] = 0.0
18
+ frequency_penalty: Optional[float] = 0.0
19
+
20
+ class Choice(BaseModel):
21
+ index: int
22
+ message: Message
23
+ finish_reason: Optional[str] = None
24
+
25
+ class Usage(BaseModel):
26
+ prompt_tokens: int = 0
27
+ completion_tokens: int = 0
28
+ total_tokens: int = 0
29
+
30
+ class ChatCompletionResponse(BaseModel):
31
+ id: str
32
+ object: Literal["chat.completion"]
33
+ created: int
34
+ model: str
35
+ choices: List[Choice]
36
+ usage: Usage = Field(default_factory=Usage)
37
+
38
+ class ErrorResponse(BaseModel):
39
+ message: str
40
+ type: str
41
+ param: Optional[str] = None
42
+ code: Optional[str] = None
43
+
44
+ class ModelList(BaseModel):
45
+ object: str = "list"
46
+ data: List[Dict]
hajimi.v0.0.4/app/services/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from app.services.gemini import GeminiClient, ResponseWrapper, GeneratedText
2
+
3
+ __all__ = [
4
+ 'GeminiClient',
5
+ 'ResponseWrapper',
6
+ 'GeneratedText'
7
+ ]
hajimi.v0.0.4/app/services/gemini.py ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+ import os
4
+ import asyncio
5
+ import time
6
+ from app.models import ChatCompletionRequest, Message
7
+ from dataclasses import dataclass
8
+ from typing import Optional, Dict, Any, List
9
+ import httpx
10
+ import logging
11
+ from app.utils import format_log_message
12
+
13
+ logger = logging.getLogger('my_logger')
14
+
15
+ # 是否启用假流式请求 默认启用
16
+ FAKE_STREAMING = os.environ.get("FAKE_STREAMING", "true").lower() in ["true", "1", "yes"]
17
+ # 假流式请求的空内容返回间隔(秒)
18
+ FAKE_STREAMING_INTERVAL = float(os.environ.get("FAKE_STREAMING_INTERVAL", "1"))
19
+
20
+ @dataclass
21
+ class GeneratedText:
22
+ text: str
23
+ finish_reason: Optional[str] = None
24
+
25
+
26
+ class ResponseWrapper:
27
+ def __init__(self, data: Dict[Any, Any]): # 正确的初始化方法名
28
+ self._data = data
29
+ self._text = self._extract_text()
30
+ self._finish_reason = self._extract_finish_reason()
31
+ self._prompt_token_count = self._extract_prompt_token_count()
32
+ self._candidates_token_count = self._extract_candidates_token_count()
33
+ self._total_token_count = self._extract_total_token_count()
34
+ self._thoughts = self._extract_thoughts()
35
+ self._json_dumps = json.dumps(self._data, indent=4, ensure_ascii=False)
36
+
37
+ def _extract_thoughts(self) -> Optional[str]:
38
+ try:
39
+ for part in self._data['candidates'][0]['content']['parts']:
40
+ if 'thought' in part:
41
+ return part['text']
42
+ return ""
43
+ except (KeyError, IndexError):
44
+ return ""
45
+
46
+ def _extract_text(self) -> str:
47
+ try:
48
+ for part in self._data['candidates'][0]['content']['parts']:
49
+ if 'thought' not in part:
50
+ return part['text']
51
+ return ""
52
+ except (KeyError, IndexError):
53
+ return ""
54
+
55
+ def _extract_finish_reason(self) -> Optional[str]:
56
+ try:
57
+ return self._data['candidates'][0].get('finishReason')
58
+ except (KeyError, IndexError):
59
+ return None
60
+
61
+ def _extract_prompt_token_count(self) -> Optional[int]:
62
+ try:
63
+ return self._data['usageMetadata'].get('promptTokenCount')
64
+ except (KeyError):
65
+ return None
66
+
67
+ def _extract_candidates_token_count(self) -> Optional[int]:
68
+ try:
69
+ return self._data['usageMetadata'].get('candidatesTokenCount')
70
+ except (KeyError):
71
+ return None
72
+
73
+ def _extract_total_token_count(self) -> Optional[int]:
74
+ try:
75
+ return self._data['usageMetadata'].get('totalTokenCount')
76
+ except (KeyError):
77
+ return None
78
+
79
+ @property
80
+ def text(self) -> str:
81
+ return self._text
82
+
83
+ @property
84
+ def finish_reason(self) -> Optional[str]:
85
+ return self._finish_reason
86
+
87
+ @property
88
+ def prompt_token_count(self) -> Optional[int]:
89
+ return self._prompt_token_count
90
+
91
+ @property
92
+ def candidates_token_count(self) -> Optional[int]:
93
+ return self._candidates_token_count
94
+
95
+ @property
96
+ def total_token_count(self) -> Optional[int]:
97
+ return self._total_token_count
98
+
99
+ @property
100
+ def thoughts(self) -> Optional[str]:
101
+ return self._thoughts
102
+
103
+ @property
104
+ def json_dumps(self) -> str:
105
+ return self._json_dumps
106
+
107
+
108
+ class GeminiClient:
109
+
110
+ AVAILABLE_MODELS = []
111
+ EXTRA_MODELS = os.environ.get("EXTRA_MODELS", "").split(",")
112
+
113
+ def __init__(self, api_key: str):
114
+ self.api_key = api_key
115
+
116
+ async def stream_chat(self, request: ChatCompletionRequest, contents, safety_settings, system_instruction):
117
+ extra_log = {'key': self.api_key[:8], 'request_type': 'stream', 'model': request.model, 'status_code': 'N/A'}
118
+ log_msg = format_log_message('INFO', "流式请求开始", extra=extra_log)
119
+ logger.info(log_msg)
120
+
121
+ # 检查是否启用假流式请求
122
+ if FAKE_STREAMING:
123
+ log_msg = format_log_message('INFO', "使用假流式请求模式(发送换行符保持连接)", extra=extra_log)
124
+ logger.info(log_msg)
125
+
126
+ try:
127
+ # 这个方法不再直接使用self.api_key,而是由main.py提供API密钥列表和管理
128
+ # 在这里,我们只负责持续发送换行符,直到main.py那边获取到响应
129
+
130
+ # 持续发送换行符,直到外部取消此生成器
131
+ start_time = time.time()
132
+ while True:
133
+ # 发送换行符作为保活消息
134
+ yield "\n"
135
+ # 等待一段时间
136
+ await asyncio.sleep(FAKE_STREAMING_INTERVAL)
137
+
138
+ # 如果等待时间过长(超过300秒),防止无限等待
139
+ if time.time() - start_time > 300:
140
+ log_msg = format_log_message('WARNING', "假流式请求等待时间过长,强制结束", extra=extra_log)
141
+ logger.warning(log_msg)
142
+ # 抛出超时异常,让外部处理
143
+ error_msg = "假流式请求等待时间过长,所有API密钥均已尝试"
144
+ extra_log_timeout = {'key': self.api_key[:8], 'request_type': 'fake-stream', 'model': request.model, 'status_code': 'TIMEOUT', 'error_message': error_msg}
145
+ log_msg = format_log_message('ERROR', error_msg, extra=extra_log_timeout)
146
+ logger.error(log_msg)
147
+ raise TimeoutError(error_msg)
148
+
149
+ except Exception as e:
150
+ if not isinstance(e, asyncio.CancelledError): # 忽略取消异常的日志记录
151
+ error_msg = f"假流式处理期间发生错误: {str(e)}"
152
+ extra_log_error = {'key': self.api_key[:8], 'request_type': 'fake-stream', 'model': request.model, 'status_code': 'ERROR', 'error_message': error_msg}
153
+ log_msg = format_log_message('ERROR', error_msg, extra=extra_log_error)
154
+ logger.error(log_msg)
155
+ raise e
156
+ finally:
157
+ log_msg = format_log_message('INFO', "假流式请求结束", extra=extra_log)
158
+ logger.info(log_msg)
159
+ else:
160
+ # 原始流式请求处理逻辑
161
+ api_version = "v1alpha" if "think" in request.model else "v1beta"
162
+ url = f"https://generativelanguage.googleapis.com/{api_version}/models/{request.model}:streamGenerateContent?key={self.api_key}&alt=sse"
163
+ headers = {
164
+ "Content-Type": "application/json",
165
+ }
166
+ data = {
167
+ "contents": contents,
168
+ "generationConfig": {
169
+ "temperature": request.temperature,
170
+ "maxOutputTokens": request.max_tokens,
171
+ },
172
+ "safetySettings": safety_settings,
173
+ }
174
+ if system_instruction:
175
+ data["system_instruction"] = system_instruction
176
+
177
+ async with httpx.AsyncClient() as client:
178
+ async with client.stream("POST", url, headers=headers, json=data, timeout=600) as response:
179
+ buffer = b""
180
+ try:
181
+ async for line in response.aiter_lines():
182
+ if not line.strip():
183
+ continue
184
+ if line.startswith("data: "):
185
+ line = line[len("data: "):]
186
+ buffer += line.encode('utf-8')
187
+ try:
188
+ data = json.loads(buffer.decode('utf-8'))
189
+ buffer = b""
190
+ if 'candidates' in data and data['candidates']:
191
+ candidate = data['candidates'][0]
192
+ if 'content' in candidate:
193
+ content = candidate['content']
194
+ if 'parts' in content and content['parts']:
195
+ parts = content['parts']
196
+ text = ""
197
+ for part in parts:
198
+ if 'text' in part:
199
+ text += part['text']
200
+ if text:
201
+ yield text
202
+
203
+ if candidate.get("finishReason") and candidate.get("finishReason") != "STOP":
204
+ error_msg = f"模型的响应被截断: {candidate.get('finishReason')}"
205
+ extra_log_error = {'key': self.api_key[:8], 'request_type': 'stream', 'model': request.model, 'status_code': 'ERROR', 'error_message': error_msg}
206
+ log_msg = format_log_message('WARNING', error_msg, extra=extra_log_error)
207
+ logger.warning(log_msg)
208
+ raise ValueError(error_msg)
209
+
210
+ if 'safetyRatings' in candidate:
211
+ for rating in candidate['safetyRatings']:
212
+ if rating['probability'] == 'HIGH':
213
+ error_msg = f"模型的响应被截断: {rating['category']}"
214
+ extra_log_safety = {'key': self.api_key[:8], 'request_type': 'stream', 'model': request.model, 'status_code': 'ERROR', 'error_message': error_msg}
215
+ log_msg = format_log_message('WARNING', error_msg, extra=extra_log_safety)
216
+ logger.warning(log_msg)
217
+ raise ValueError(error_msg)
218
+ except json.JSONDecodeError:
219
+ continue
220
+ except Exception as e:
221
+ error_msg = f"流式处理期间发生错误: {str(e)}"
222
+ extra_log_stream_error = {'key': self.api_key[:8], 'request_type': 'stream', 'model': request.model, 'status_code': 'ERROR', 'error_message': error_msg}
223
+ log_msg = format_log_message('ERROR', error_msg, extra=extra_log_stream_error)
224
+ logger.error(log_msg)
225
+ raise e
226
+ except Exception as e:
227
+ raise e
228
+ finally:
229
+ log_msg = format_log_message('INFO', "流式请求结束", extra=extra_log)
230
+ logger.info(log_msg)
231
+
232
+ def complete_chat(self, request: ChatCompletionRequest, contents, safety_settings, system_instruction):
233
+ extra_log = {'key': self.api_key[:8], 'request_type': 'non-stream', 'model': request.model, 'status_code': 'N/A'}
234
+ log_msg = format_log_message('INFO', "非流式请求开始", extra=extra_log)
235
+ logger.info(log_msg)
236
+
237
+ api_version = "v1alpha" if "think" in request.model else "v1beta"
238
+ url = f"https://generativelanguage.googleapis.com/{api_version}/models/{request.model}:generateContent?key={self.api_key}"
239
+ headers = {
240
+ "Content-Type": "application/json",
241
+ }
242
+ data = {
243
+ "contents": contents,
244
+ "generationConfig": {
245
+ "temperature": request.temperature,
246
+ "maxOutputTokens": request.max_tokens,
247
+ },
248
+ "safetySettings": safety_settings,
249
+ }
250
+ if system_instruction:
251
+ data["system_instruction"] = system_instruction
252
+
253
+ try:
254
+ response = requests.post(url, headers=headers, json=data)
255
+ response.raise_for_status()
256
+
257
+ log_msg = format_log_message('INFO', "非流式请求成功完成", extra=extra_log)
258
+ logger.info(log_msg)
259
+
260
+ return ResponseWrapper(response.json())
261
+ except Exception as e:
262
+ raise
263
+
264
+ def convert_messages(self, messages, use_system_prompt=False):
265
+ gemini_history = []
266
+ errors = []
267
+ system_instruction_text = ""
268
+ is_system_phase = use_system_prompt
269
+ for i, message in enumerate(messages):
270
+ role = message.role
271
+ content = message.content
272
+
273
+ if isinstance(content, str):
274
+ if is_system_phase and role == 'system':
275
+ if system_instruction_text:
276
+ system_instruction_text += "\n" + content
277
+ else:
278
+ system_instruction_text = content
279
+ else:
280
+ is_system_phase = False
281
+
282
+ if role in ['user', 'system']:
283
+ role_to_use = 'user'
284
+ elif role == 'assistant':
285
+ role_to_use = 'model'
286
+ else:
287
+ errors.append(f"Invalid role: {role}")
288
+ continue
289
+
290
+ if gemini_history and gemini_history[-1]['role'] == role_to_use:
291
+ gemini_history[-1]['parts'].append({"text": content})
292
+ else:
293
+ gemini_history.append(
294
+ {"role": role_to_use, "parts": [{"text": content}]})
295
+ elif isinstance(content, list):
296
+ parts = []
297
+ for item in content:
298
+ if item.get('type') == 'text':
299
+ parts.append({"text": item.get('text')})
300
+ elif item.get('type') == 'image_url':
301
+ image_data = item.get('image_url', {}).get('url', '')
302
+ if image_data.startswith('data:image/'):
303
+ try:
304
+ mime_type, base64_data = image_data.split(';')[0].split(':')[1], image_data.split(',')[1]
305
+ parts.append({
306
+ "inline_data": {
307
+ "mime_type": mime_type,
308
+ "data": base64_data
309
+ }
310
+ })
311
+ except (IndexError, ValueError):
312
+ errors.append(
313
+ f"Invalid data URI for image: {image_data}")
314
+ else:
315
+ errors.append(
316
+ f"Invalid image URL format for item: {item}")
317
+
318
+ if parts:
319
+ if role in ['user', 'system']:
320
+ role_to_use = 'user'
321
+ elif role == 'assistant':
322
+ role_to_use = 'model'
323
+ else:
324
+ errors.append(f"Invalid role: {role}")
325
+ continue
326
+ if gemini_history and gemini_history[-1]['role'] == role_to_use:
327
+ gemini_history[-1]['parts'].extend(parts)
328
+ else:
329
+ gemini_history.append(
330
+ {"role": role_to_use, "parts": parts})
331
+ if errors:
332
+ return errors
333
+ else:
334
+ return gemini_history, {"parts": [{"text": system_instruction_text}]}
335
+
336
+ @staticmethod
337
+ async def list_available_models(api_key) -> list:
338
+ url = "https://generativelanguage.googleapis.com/v1beta/models?key={}".format(
339
+ api_key)
340
+ async with httpx.AsyncClient() as client:
341
+ response = await client.get(url)
342
+ response.raise_for_status()
343
+ data = response.json()
344
+ models = [model["name"] for model in data.get("models", [])]
345
+ models.extend(GeminiClient.EXTRA_MODELS)
346
+ return models
hajimi.v0.0.4/app/templates/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Templates package initialization
hajimi.v0.0.4/app/templates/index.html ADDED
@@ -0,0 +1,576 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Gemini API 代理服务</title>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
10
+ max-width: 1200px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ line-height: 1.6;
14
+ background-color: #f8f9fa;
15
+ }
16
+ h1, h2, h3 {
17
+ color: #333;
18
+ text-align: center;
19
+ margin-bottom: 20px;
20
+ }
21
+ .info-box {
22
+ background-color: #fff;
23
+ border: 1px solid #dee2e6;
24
+ border-radius: 8px;
25
+ padding: 20px;
26
+ margin-bottom: 20px;
27
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
28
+ }
29
+ .status {
30
+ color: #28a745;
31
+ font-weight: bold;
32
+ font-size: 18px;
33
+ margin-bottom: 20px;
34
+ text-align: center;
35
+ }
36
+ .stats-grid {
37
+ display: grid;
38
+ grid-template-columns: repeat(3, 1fr);
39
+ gap: 15px;
40
+ margin-top: 15px;
41
+ margin-bottom: 20px;
42
+ }
43
+ .stat-card {
44
+ background-color: #e9ecef;
45
+ padding: 15px;
46
+ border-radius: 8px;
47
+ text-align: center;
48
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
49
+ transition: transform 0.2s;
50
+ }
51
+ .stat-card:hover {
52
+ transform: translateY(-2px);
53
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
54
+ }
55
+ .stat-value {
56
+ font-size: 24px;
57
+ font-weight: bold;
58
+ color: #007bff;
59
+ }
60
+ .stat-label {
61
+ font-size: 14px;
62
+ color: #6c757d;
63
+ margin-top: 5px;
64
+ }
65
+ .section-title {
66
+ color: #495057;
67
+ border-bottom: 1px solid #dee2e6;
68
+ padding-bottom: 10px;
69
+ margin-bottom: 20px;
70
+ }
71
+ .log-container {
72
+ background-color: #f5f5f5;
73
+ border: 1px solid #ddd;
74
+ border-radius: 8px;
75
+ padding: 15px;
76
+ margin-top: 20px;
77
+ max-height: 500px;
78
+ overflow-y: auto;
79
+ font-family: monospace;
80
+ font-size: 14px;
81
+ line-height: 1.5;
82
+ }
83
+ .log-entry {
84
+ margin-bottom: 8px;
85
+ padding: 8px;
86
+ border-radius: 4px;
87
+ }
88
+ .log-entry.INFO {
89
+ background-color: #e8f4f8;
90
+ border-left: 4px solid #17a2b8;
91
+ }
92
+ .log-entry.WARNING {
93
+ background-color: #fff3cd;
94
+ border-left: 4px solid #ffc107;
95
+ }
96
+ .log-entry.ERROR {
97
+ background-color: #f8d7da;
98
+ border-left: 4px solid #dc3545;
99
+ }
100
+ .log-entry.DEBUG {
101
+ background-color: #d1ecf1;
102
+ border-left: 4px solid #17a2b8;
103
+ }
104
+ .log-timestamp {
105
+ color: #6c757d;
106
+ font-size: 12px;
107
+ margin-right: 10px;
108
+ }
109
+ .log-level {
110
+ font-weight: bold;
111
+ margin-right: 10px;
112
+ }
113
+ .log-level.INFO {
114
+ color: #17a2b8;
115
+ }
116
+ .log-level.WARNING {
117
+ color: #ffc107;
118
+ }
119
+ .log-level.ERROR {
120
+ color: #dc3545;
121
+ }
122
+ .log-level.DEBUG {
123
+ color: #17a2b8;
124
+ }
125
+ .log-message {
126
+ color: #212529;
127
+ }
128
+ .refresh-button {
129
+ display: block;
130
+ margin: 20px auto;
131
+ padding: 10px 20px;
132
+ background-color: #007bff;
133
+ color: white;
134
+ border: none;
135
+ border-radius: 4px;
136
+ font-size: 16px;
137
+ cursor: pointer;
138
+ transition: background-color 0.2s;
139
+ }
140
+ .refresh-button:hover {
141
+ background-color: #0069d9;
142
+ }
143
+ .log-filter {
144
+ display: flex;
145
+ justify-content: center;
146
+ margin-bottom: 15px;
147
+ gap: 10px;
148
+ }
149
+ .log-filter button {
150
+ padding: 5px 10px;
151
+ border: 1px solid #ddd;
152
+ border-radius: 4px;
153
+ background-color: #f8f9fa;
154
+ cursor: pointer;
155
+ }
156
+ .log-filter button.active {
157
+ background-color: #007bff;
158
+ color: white;
159
+ border-color: #007bff;
160
+ }
161
+
162
+ /* API密钥统计样式 */
163
+ .api-key-stats-container {
164
+ margin-top: 20px;
165
+ }
166
+
167
+ .api-key-stats-list {
168
+ display: grid;
169
+ grid-template-columns: repeat(3, 1fr); /* 电脑上显示为三列 */
170
+ gap: 15px;
171
+ margin-top: 15px;
172
+ }
173
+
174
+ /* 在中等屏幕上显示为两列 */
175
+ @media (max-width: 992px) {
176
+ .api-key-stats-list {
177
+ grid-template-columns: repeat(2, 1fr);
178
+ }
179
+ }
180
+
181
+ /* 在小屏幕上显示为一列 */
182
+ @media (max-width: 576px) {
183
+ .api-key-stats-list {
184
+ grid-template-columns: 1fr;
185
+ }
186
+ }
187
+
188
+ .api-key-item {
189
+ background-color: #f8f9fa;
190
+ border-radius: 8px;
191
+ padding: 15px;
192
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
193
+ }
194
+
195
+ .api-key-header {
196
+ display: flex;
197
+ justify-content: space-between;
198
+ align-items: center;
199
+ margin-bottom: 10px;
200
+ }
201
+
202
+ .api-key-name {
203
+ font-weight: bold;
204
+ color: #495057;
205
+ }
206
+
207
+ .api-key-usage {
208
+ display: flex;
209
+ align-items: center;
210
+ gap: 10px;
211
+ }
212
+
213
+ .api-key-count {
214
+ font-weight: bold;
215
+ color: #007bff;
216
+ }
217
+
218
+ .progress-container {
219
+ width: 100%;
220
+ height: 10px;
221
+ background-color: #e9ecef;
222
+ border-radius: 5px;
223
+ overflow: hidden;
224
+ }
225
+
226
+ .progress-bar {
227
+ height: 100%;
228
+ border-radius: 5px;
229
+ transition: width 0.3s ease;
230
+ }
231
+
232
+ .progress-bar.low {
233
+ background-color: #28a745; /* 绿色 - 低使用率 */
234
+ }
235
+
236
+ .progress-bar.medium {
237
+ background-color: #ffc107; /* 黄色 - 中等使用率 */
238
+ }
239
+
240
+ .progress-bar.high {
241
+ background-color: #dc3545; /* 红色 - 高使用率 */
242
+ }
243
+ </style>
244
+ </head>
245
+ <body>
246
+ <h1>🤖 Gemini API 代理服务</h1>
247
+
248
+ <div class="info-box">
249
+ <h2 class="section-title">🟢 运行状态</h2>
250
+ <p class="status">服务运行中</p>
251
+
252
+ <div class="stats-grid">
253
+ <div class="stat-card">
254
+ <div class="stat-value">{{ key_count }}</div>
255
+ <div class="stat-label">可用API密钥数量</div>
256
+ </div>
257
+ <div class="stat-card">
258
+ <div class="stat-value">{{ model_count }}</div>
259
+ <div class="stat-label">可用模型数量</div>
260
+ </div>
261
+ <div class="stat-card">
262
+ <div class="stat-value">{{ retry_count }}</div>
263
+ <div class="stat-label">最大重试次数</div>
264
+ </div>
265
+ </div>
266
+
267
+ <h3 class="section-title">API调用统计</h3>
268
+ <div class="stats-grid">
269
+ <div class="stat-card">
270
+ <div class="stat-value" id="last-24h-calls">{{ last_24h_calls }}</div>
271
+ <div class="stat-label">24小时内调用次数</div>
272
+ </div>
273
+ <div class="stat-card">
274
+ <div class="stat-value" id="hourly-calls">{{ hourly_calls }}</div>
275
+ <div class="stat-label">一小时内调用次数</div>
276
+ </div>
277
+ <div class="stat-card">
278
+ <div class="stat-value" id="minute-calls">{{ minute_calls }}</div>
279
+ <div class="stat-label">一分钟内调用次数</div>
280
+ </div>
281
+ </div>
282
+
283
+ <div class="api-key-stats-container">
284
+ <h3 class="section-title" style="cursor: pointer; user-select: none;" onclick="toggleApiKeyStats()">
285
+ API密钥使用统计 <span id="toggle-icon">▼</span>
286
+ </h3>
287
+ <div id="api-key-stats" style="display: block;">
288
+ <div class="api-key-stats-list" id="api-key-stats-list">
289
+ <!-- API密钥统计将在这里动态生成 -->
290
+ </div>
291
+ </div>
292
+ </div>
293
+ </div>
294
+
295
+ <div class="info-box">
296
+ <h2 class="section-title">⚙️ 环境配置</h2>
297
+ <div class="stats-grid">
298
+ <div class="stat-card">
299
+ <div class="stat-value">{{ max_requests_per_minute }}</div>
300
+ <div class="stat-label">每分钟请求限制</div>
301
+ </div>
302
+ <div class="stat-card">
303
+ <div class="stat-value">{{ max_requests_per_day_per_ip }}</div>
304
+ <div class="stat-label">每IP每日请求限制</div>
305
+ </div>
306
+ <div class="stat-card">
307
+ <div class="stat-value" id="current-time">{{ current_time }}</div>
308
+ <div class="stat-label">当前服务器时间</div>
309
+ </div>
310
+ </div>
311
+ </div>
312
+
313
+ <div class="info-box">
314
+ <h2 class="section-title">📦 版本信息</h2>
315
+ <div class="version-info" style="text-align: center; margin-bottom: 15px;">
316
+ <div style="font-size: 18px; margin-bottom: 10px;">
317
+ 当前版本: <span style="font-weight: bold; color: #007bff;">{{ local_version }}</span>
318
+ </div>
319
+ {% if has_update %}
320
+ <div style="display: flex; align-items: center; justify-content: center; margin-top: 15px;">
321
+ <div style="background-color: #fef6e0; border: 1px solid #ffeeba; border-radius: 4px; padding: 10px 15px; display: inline-flex; align-items: center;">
322
+ <span style="color: #ff9800; margin-right: 10px;">
323
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
324
+ <circle cx="12" cy="12" r="10"></circle>
325
+ <line x1="12" y1="8" x2="12" y2="12"></line>
326
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
327
+ </svg>
328
+ </span>
329
+ <span>
330
+ <strong>发现新版本!</strong> 最新版本: <span style="font-weight: bold; color: #28a745;">{{ remote_version }}</span>
331
+ </span>
332
+ </div>
333
+ </div>
334
+ {% endif %}
335
+ </div>
336
+ </div>
337
+
338
+ <div class="info-box">
339
+ <h2 class="section-title"> 系统日志</h2>
340
+ <div class="log-filter">
341
+ <button class="active" data-level="ALL">全部</button>
342
+ <button data-level="INFO">信息</button>
343
+ <button data-level="WARNING">警告</button>
344
+ <button data-level="ERROR">错误</button>
345
+ </div>
346
+ <div class="log-container" id="log-container">
347
+ {% for log in logs %}
348
+ <div class="log-entry {{ log.level }}" data-level="{{ log.level }}">
349
+ <span class="log-timestamp">{{ log.timestamp }}</span>
350
+ <span class="log-level {{ log.level }}">{{ log.level }}</span>
351
+ <span class="log-message">
352
+ {% if log.key != 'N/A' %}[{{ log.key }}]{% endif %}
353
+ {% if log.request_type != 'N/A' %}{{ log.request_type }}{% endif %}
354
+ {% if log.model != 'N/A' %}[{{ log.model }}]{% endif %}
355
+ {% if log.status_code != 'N/A' %}{{ log.status_code }}{% endif %}
356
+ : {{ log.message }}
357
+ {% if log.error_message %}
358
+ - {{ log.error_message }}
359
+ {% endif %}
360
+ </span>
361
+ </div>
362
+ {% endfor %}
363
+ </div>
364
+ <button class="refresh-button" id="refresh-button">刷新数据</button>
365
+ </div>
366
+
367
+ <script>
368
+ // 日志过滤功能
369
+ document.querySelectorAll('.log-filter button').forEach(button => {
370
+ button.addEventListener('click', function() {
371
+ // 移除所有按钮的active类
372
+ document.querySelectorAll('.log-filter button').forEach(btn => {
373
+ btn.classList.remove('active');
374
+ });
375
+
376
+ // 为当前按钮添加active类
377
+ this.classList.add('active');
378
+
379
+ const level = this.getAttribute('data-level');
380
+
381
+ // 显示或隐藏日志条目
382
+ document.querySelectorAll('.log-entry').forEach(entry => {
383
+ if (level === 'ALL' || entry.getAttribute('data-level') === level) {
384
+ entry.style.display = 'block';
385
+ } else {
386
+ entry.style.display = 'none';
387
+ }
388
+ });
389
+ });
390
+ });
391
+
392
+ // 动态刷新功能
393
+ let refreshInterval;
394
+ const refreshRate = 1000; // 1秒刷新一次
395
+ let isRefreshing = false;
396
+
397
+ // 开始自动刷新
398
+ function startAutoRefresh() {
399
+ if (!refreshInterval) {
400
+ refreshInterval = setInterval(fetchDashboardData, refreshRate);
401
+ console.log('自动刷新已启动');
402
+ }
403
+ }
404
+
405
+ // 停止自动刷新
406
+ function stopAutoRefresh() {
407
+ if (refreshInterval) {
408
+ clearInterval(refreshInterval);
409
+ refreshInterval = null;
410
+ console.log('自动刷新已停止');
411
+ }
412
+ }
413
+
414
+ // 获取仪表盘数据
415
+ async function fetchDashboardData() {
416
+ if (isRefreshing) return; // 防止重复请求
417
+
418
+ isRefreshing = true;
419
+ try {
420
+ const response = await fetch('/api/dashboard-data');
421
+ if (!response.ok) {
422
+ throw new Error(`HTTP error! status: ${response.status}`);
423
+ }
424
+ const data = await response.json();
425
+ updateDashboard(data);
426
+ } catch (error) {
427
+ console.error('获取数据失败:', error);
428
+ } finally {
429
+ isRefreshing = false;
430
+ }
431
+ }
432
+
433
+ // 更新仪表盘数据
434
+ function updateDashboard(data) {
435
+ // 更新时间
436
+ document.getElementById('current-time').textContent = data.current_time;
437
+
438
+ // 更新调用统计
439
+ document.getElementById('last-24h-calls').textContent = data.last_24h_calls;
440
+ document.getElementById('hourly-calls').textContent = data.hourly_calls;
441
+ document.getElementById('minute-calls').textContent = data.minute_calls;
442
+
443
+ // 更新API密钥统计
444
+ if (data.api_key_stats) {
445
+ updateApiKeyStats(data.api_key_stats);
446
+ }
447
+
448
+ // 更新日志
449
+ updateLogs(data.logs);
450
+ }
451
+
452
+ // 更新API密钥统计
453
+ function updateApiKeyStats(apiKeyStats) {
454
+ const container = document.getElementById('api-key-stats-list');
455
+ container.innerHTML = '';
456
+
457
+ if (!apiKeyStats || apiKeyStats.length === 0) {
458
+ container.innerHTML = '<div class="api-key-item">没有API密钥使用数据</div>';
459
+ return;
460
+ }
461
+
462
+ apiKeyStats.forEach(stat => {
463
+ const item = document.createElement('div');
464
+ item.className = 'api-key-item';
465
+
466
+ // 确定进度条颜色
467
+ let barClass = 'low';
468
+ if (stat.usage_percent > 75) {
469
+ barClass = 'high';
470
+ } else if (stat.usage_percent > 50) {
471
+ barClass = 'medium';
472
+ }
473
+
474
+ item.innerHTML = `
475
+ <div class="api-key-header">
476
+ <div class="api-key-name">API密钥: ${stat.api_key}</div>
477
+ <div class="api-key-usage">
478
+ <span class="api-key-count">${stat.calls_24h}</span> /
479
+ <span class="api-key-limit">${stat.limit}</span>
480
+ <span class="api-key-percent">(${stat.usage_percent}%)</span>
481
+ </div>
482
+ </div>
483
+ <div class="progress-container">
484
+ <div class="progress-bar ${barClass}" style="width: ${Math.min(stat.usage_percent, 100)}%"></div>
485
+ </div>
486
+ `;
487
+
488
+ container.appendChild(item);
489
+ });
490
+ }
491
+
492
+ // 切换API密钥统计显示/隐藏
493
+ function toggleApiKeyStats() {
494
+ const statsDiv = document.getElementById('api-key-stats');
495
+ const toggleIcon = document.getElementById('toggle-icon');
496
+
497
+ if (statsDiv.style.display === 'none') {
498
+ statsDiv.style.display = 'block';
499
+ toggleIcon.textContent = '▼';
500
+ } else {
501
+ statsDiv.style.display = 'none';
502
+ toggleIcon.textContent = '▶';
503
+ }
504
+ }
505
+
506
+ // 更新日志
507
+ function updateLogs(logs) {
508
+ const logContainer = document.getElementById('log-container');
509
+ const currentFilter = document.querySelector('.log-filter button.active').getAttribute('data-level');
510
+
511
+ // 保存当前滚动位置
512
+ const wasScrolledToBottom = logContainer.scrollHeight - logContainer.clientHeight <= logContainer.scrollTop + 5;
513
+
514
+ // 清空现有日志
515
+ logContainer.innerHTML = '';
516
+
517
+ // 添加新日志
518
+ logs.forEach(log => {
519
+ const logEntry = document.createElement('div');
520
+ logEntry.className = `log-entry ${log.level}`;
521
+ logEntry.setAttribute('data-level', log.level);
522
+
523
+ // 根据当前过滤器设置显示状态
524
+ if (currentFilter !== 'ALL' && log.level !== currentFilter) {
525
+ logEntry.style.display = 'none';
526
+ }
527
+
528
+ const timestampSpan = document.createElement('span');
529
+ timestampSpan.className = 'log-timestamp';
530
+ timestampSpan.textContent = log.timestamp;
531
+
532
+ const levelSpan = document.createElement('span');
533
+ levelSpan.className = `log-level ${log.level}`;
534
+ levelSpan.textContent = log.level;
535
+
536
+ const messageSpan = document.createElement('span');
537
+ messageSpan.className = 'log-message';
538
+
539
+ // 构建消息内容
540
+ let messageContent = '';
541
+ if (log.key !== 'N/A') messageContent += `[${log.key}] `;
542
+ if (log.request_type !== 'N/A') messageContent += log.request_type + ' ';
543
+ if (log.model !== 'N/A') messageContent += `[${log.model}] `;
544
+ if (log.status_code !== 'N/A') messageContent += log.status_code + ' ';
545
+ messageContent += ': ' + log.message;
546
+ if (log.error_message) messageContent += ' - ' + log.error_message;
547
+
548
+ messageSpan.textContent = messageContent;
549
+
550
+ logEntry.appendChild(timestampSpan);
551
+ logEntry.appendChild(levelSpan);
552
+ logEntry.appendChild(messageSpan);
553
+
554
+ logContainer.appendChild(logEntry);
555
+ });
556
+
557
+ // 如果之前是滚动到底部的,则保持滚动到底部
558
+ if (wasScrolledToBottom) {
559
+ logContainer.scrollTop = logContainer.scrollHeight;
560
+ }
561
+ }
562
+
563
+ // 页面加载时自动滚动到日志底部并启动自动刷新
564
+ window.onload = function() {
565
+ const logContainer = document.getElementById('log-container');
566
+ logContainer.scrollTop = logContainer.scrollHeight;
567
+
568
+ // 启动自动刷新
569
+ startAutoRefresh();
570
+ };
571
+
572
+ // 手动刷新按钮
573
+ document.getElementById('refresh-button').addEventListener('click', fetchDashboardData);
574
+ </script>
575
+ </body>
576
+ </html>
hajimi.v0.0.4/app/utils/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Utils package initialization
2
+
3
+ from app.utils.logging import logger, log_manager, format_log_message,log
4
+ from app.utils.api_key import APIKeyManager, test_api_key
5
+ from app.utils.error_handling import handle_gemini_error, translate_error, handle_api_error
6
+ from app.utils.rate_limiting import protect_from_abuse
7
+ from app.utils.cache import ResponseCacheManager, generate_cache_key, cache_response
8
+ from app.utils.request import ActiveRequestsManager, check_client_disconnect
9
+ from app.utils.stats import clean_expired_stats, update_api_call_stats
10
+ from app.utils.response import create_chat_response, create_error_response, create_response, handle_exception
11
+ from app.utils.version import check_version
12
+ from app.utils.maintenance import handle_exception, schedule_cache_cleanup
hajimi.v0.0.4/app/utils/api_key.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import re
3
+ import os
4
+ import logging
5
+ from datetime import datetime, timedelta
6
+ from apscheduler.schedulers.background import BackgroundScheduler
7
+ from app.utils.logging import format_log_message
8
+
9
+ logger = logging.getLogger("my_logger")
10
+
11
+ class APIKeyManager:
12
+ def __init__(self):
13
+ self.api_keys = re.findall(
14
+ r"AIzaSy[a-zA-Z0-9_-]{33}", os.environ.get('GEMINI_API_KEYS', ""))
15
+ self.key_stack = [] # 初始化密钥栈
16
+ self._reset_key_stack() # 初始化时创建随机密钥栈
17
+ # self.api_key_blacklist = set()
18
+ # self.api_key_blacklist_duration = 60
19
+ self.scheduler = BackgroundScheduler()
20
+ self.scheduler.start()
21
+ self.tried_keys_for_request = set() # 用于跟踪当前请求尝试中已试过的 key
22
+
23
+ def _reset_key_stack(self):
24
+ """创建并随机化密钥栈"""
25
+ shuffled_keys = self.api_keys[:] # 创建 api_keys 的副本以避免直接修改原列表
26
+ random.shuffle(shuffled_keys)
27
+ self.key_stack = shuffled_keys
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:
35
+ if key not in self.tried_keys_for_request:
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:
48
+ key = self.key_stack.pop()
49
+ # if key not in self.api_key_blacklist and key not in self.tried_keys_for_request:
50
+ if key not in self.tried_keys_for_request:
51
+ self.tried_keys_for_request.add(key)
52
+ return key
53
+
54
+ return None
55
+
56
+
57
+ def show_all_keys(self):
58
+ log_msg = format_log_message('INFO', f"当前可用API key个数: {len(self.api_keys)} ")
59
+ logger.info(log_msg)
60
+ for i, api_key in enumerate(self.api_keys):
61
+ log_msg = format_log_message('INFO', f"API Key{i}: {api_key[:8]}...{api_key[-3:]}")
62
+ logger.info(log_msg)
63
+
64
+ # def blacklist_key(self, key):
65
+ # log_msg = format_log_message('WARNING', f"{key[:8]} → 暂时禁用 {self.api_key_blacklist_duration} 秒")
66
+ # logger.warning(log_msg)
67
+ # self.api_key_blacklist.add(key)
68
+ # self.scheduler.add_job(lambda: self.api_key_blacklist.discard(key), 'date',
69
+ # run_date=datetime.now() + timedelta(seconds=self.api_key_blacklist_duration))
70
+
71
+ def reset_tried_keys_for_request(self):
72
+ """在新的请求尝试时重置已尝试的 key 集合"""
73
+ self.tried_keys_for_request = set()
74
+
75
+ async def test_api_key(api_key: str) -> bool:
76
+ """
77
+ 测试 API 密钥是否有效。
78
+ """
79
+ try:
80
+ import httpx
81
+ url = "https://generativelanguage.googleapis.com/v1beta/models?key={}".format(api_key)
82
+ async with httpx.AsyncClient() as client:
83
+ response = await client.get(url)
84
+ response.raise_for_status()
85
+ return True
86
+ except Exception:
87
+ return False
hajimi.v0.0.4/app/utils/cache.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import hashlib
3
+ import json
4
+ from typing import Dict, Any, Optional
5
+ import logging
6
+ from app.utils.logging import log
7
+ from app.config.settings import (
8
+ api_call_stats
9
+ )
10
+ logger = logging.getLogger("my_logger")
11
+
12
+ class ResponseCacheManager:
13
+ """管理API响应缓存的类"""
14
+
15
+ def __init__(self, expiry_time: int, max_entries: int, remove_after_use: bool = True,
16
+ cache_dict: Dict[str, Dict[str, Any]] = None):
17
+ self.cache = cache_dict if cache_dict is not None else {} # 使用传入的缓存字典或创建新字典
18
+ self.expiry_time = expiry_time
19
+ self.max_entries = max_entries
20
+ self.remove_after_use = remove_after_use
21
+
22
+ def get(self, cache_key: str):
23
+ """获取缓存项,如果存在且未过期"""
24
+ now = time.time()
25
+ if cache_key in self.cache and now < self.cache[cache_key].get('expiry_time', 0):
26
+ cached_item = self.cache[cache_key]
27
+
28
+ # 获取响应但先不删除
29
+ response = cached_item['response']
30
+
31
+ # 返回响应
32
+ return response, True
33
+
34
+ return None, False
35
+
36
+ def store(self, cache_key: str, response, client_ip: str = None):
37
+ """存储响应到缓存"""
38
+ now = time.time()
39
+ self.cache[cache_key] = {
40
+ 'response': response,
41
+ 'expiry_time': now + self.expiry_time,
42
+ 'created_at': now,
43
+ 'client_ip': client_ip
44
+ }
45
+
46
+ log('info', f"响应已缓存: {cache_key[:8]}...",
47
+ extra={'cache_operation': 'store', 'request_type': 'non-stream'})
48
+
49
+ # 如果缓存超过限制,清理最旧的
50
+ self.clean_if_needed()
51
+
52
+ def clean_expired(self):
53
+ """清理所有过期的缓存项"""
54
+ now = time.time()
55
+ expired_keys = [k for k, v in self.cache.items() if now > v.get('expiry_time', 0)]
56
+
57
+ for key in expired_keys:
58
+ del self.cache[key]
59
+ log('info', f"清理过期缓存: {key[:8]}...", extra={'cache_operation': 'clean'})
60
+
61
+ def clean_if_needed(self):
62
+ """如果缓存数量超过限制,清理最旧的项目"""
63
+ if len(self.cache) <= self.max_entries:
64
+ return
65
+
66
+ # 按创建时间排序
67
+ sorted_keys = sorted(self.cache.keys(),
68
+ key=lambda k: self.cache[k].get('created_at', 0))
69
+
70
+ # 计算需要删除的数量
71
+ to_remove = len(self.cache) - self.max_entries
72
+
73
+ # 删除最旧的项
74
+ for key in sorted_keys[:to_remove]:
75
+ del self.cache[key]
76
+ log('info', f"缓存容量限制,删除旧缓存: {key[:8]}...", extra={'cache_operation': 'limit'})
77
+
78
+ def generate_cache_key(chat_request) -> str:
79
+ """生成请求的唯一缓存键"""
80
+ # 创建包含请求关键信息的字典
81
+ request_data = {
82
+ 'model': chat_request.model,
83
+ 'messages': []
84
+ }
85
+
86
+ # 添加消息内容
87
+ for msg in chat_request.messages:
88
+ if isinstance(msg.content, str):
89
+ message_data = {'role': msg.role, 'content': msg.content}
90
+ request_data['messages'].append(message_data)
91
+ elif isinstance(msg.content, list):
92
+ content_list = []
93
+ for item in msg.content:
94
+ if item.get('type') == 'text':
95
+ content_list.append({'type': 'text', 'text': item.get('text')})
96
+ # 对于图像数据,我们只使用标识符而不是全部数据
97
+ elif item.get('type') == 'image_url':
98
+ image_data = item.get('image_url', {}).get('url', '')
99
+ if image_data.startswith('data:image/'):
100
+ # 对于base64图像,使用前32字符作为标识符
101
+ content_list.append({'type': 'image_url', 'hash': hashlib.md5(image_data[:32].encode()).hexdigest()})
102
+ else:
103
+ content_list.append({'type': 'image_url', 'url': image_data})
104
+ request_data['messages'].append({'role': msg.role, 'content': content_list})
105
+
106
+ # 将字典转换为JSON字符串并计算哈希值
107
+ json_data = json.dumps(request_data, sort_keys=True)
108
+ return hashlib.md5(json_data.encode()).hexdigest()
109
+
110
+ def cache_response(response, cache_key, client_ip, response_cache_manager, update_api_call_stats, api_key=None):
111
+ """
112
+ 将响应存入缓存
113
+
114
+ 参数:
115
+ - response: 响应对象
116
+ - cache_key: 缓存键
117
+ - client_ip: 客户端IP
118
+ - response_cache_manager: 缓存管理器
119
+ - update_api_call_stats: 更新统计的函数
120
+ - api_key: API密钥,用于更新API密钥使用统计
121
+ """
122
+ if not cache_key:
123
+ return
124
+
125
+ # 先检查缓存是否已存在
126
+ existing_cache = cache_key in response_cache_manager.cache
127
+
128
+ if existing_cache:
129
+ log('info', f"缓存已存在,跳过存储: {cache_key[:8]}...",
130
+ extra={'cache_operation': 'skip-existing', 'request_type': 'non-stream'})
131
+ else:
132
+ response_cache_manager.store(cache_key, response, client_ip)
133
+ log('info', f"API响应已缓存: {cache_key[:8]}...",
134
+ extra={'cache_operation': 'store-new', 'request_type': 'non-stream'})
135
+
136
+ # 更新API调用统计
137
+ update_api_call_stats(api_call_stats, api_key)
hajimi.v0.0.4/app/utils/error_handling.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ 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
+
9
+ def handle_gemini_error(error, current_api_key, key_manager) -> str:
10
+ if isinstance(error, requests.exceptions.HTTPError):
11
+ status_code = error.response.status_code
12
+ if status_code == 400:
13
+ try:
14
+ error_data = error.response.json()
15
+ if 'error' in error_data:
16
+ if error_data['error'].get('code') == "invalid_argument":
17
+ error_message = "无效的 API 密钥"
18
+ extra_log_invalid_key = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
19
+ log_msg = format_log_message('ERROR', f"{current_api_key[:8]} ... {current_api_key[-3:]} → 无效,可能已过期或被删除", extra=extra_log_invalid_key)
20
+ logger.error(log_msg)
21
+ # key_manager.blacklist_key(current_api_key)
22
+
23
+ return error_message
24
+ error_message = error_data['error'].get(
25
+ 'message', 'Bad Request')
26
+ extra_log_400 = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
27
+ log_msg = format_log_message('WARNING', f"400 错误请求: {error_message}", extra=extra_log_400)
28
+ logger.warning(log_msg)
29
+ return f"400 错误请求: {error_message}"
30
+ except ValueError:
31
+ error_message = "400 错误请求:响应不是有效的JSON格式"
32
+ extra_log_400_json = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
33
+ log_msg = format_log_message('WARNING', error_message, extra=extra_log_400_json)
34
+ logger.warning(log_msg)
35
+ return error_message
36
+
37
+ elif status_code == 429:
38
+ error_message = "API 密钥配额已用尽或其他原因"
39
+ extra_log_429 = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
40
+ log_msg = format_log_message('WARNING', f"{current_api_key[:8]} ... {current_api_key[-3:]} → 429 官方资源耗尽或其他原因", extra=extra_log_429)
41
+ logger.warning(log_msg)
42
+ # key_manager.blacklist_key(current_api_key)
43
+
44
+ return error_message
45
+
46
+ elif status_code == 403:
47
+ error_message = "权限被拒绝"
48
+ extra_log_403 = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
49
+ log_msg = format_log_message('ERROR', f"{current_api_key[:8]} ... {current_api_key[-3:]} → 403 权限被拒绝", extra=extra_log_403)
50
+ logger.error(log_msg)
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}
72
+ log_msg = format_log_message('WARNING', f"{current_api_key[:8]} ... {current_api_key[-3:]} → {status_code} 未知错误", extra=extra_log_other)
73
+ logger.warning(log_msg)
74
+
75
+ return f"未知错误/模型不可用: {status_code}"
76
+
77
+ elif isinstance(error, requests.exceptions.ConnectionError):
78
+ error_message = "连接错误"
79
+ log_msg = format_log_message('WARNING', error_message, extra={'error_message': error_message})
80
+ logger.warning(log_msg)
81
+ return error_message
82
+
83
+ elif isinstance(error, requests.exceptions.Timeout):
84
+ error_message = "请求超时"
85
+ log_msg = format_log_message('WARNING', error_message, extra={'error_message': error_message})
86
+ logger.warning(log_msg)
87
+ return error_message
88
+ else:
89
+ error_message = f"发生未知错误: {error}"
90
+ log_msg = format_log_message('ERROR', error_message, extra={'error_message': error_message})
91
+ logger.error(log_msg)
92
+ return error_message
93
+
94
+ def translate_error(message: str) -> str:
95
+ if "quota exceeded" in message.lower():
96
+ return "API 密钥配额已用尽"
97
+ if "invalid argument" in message.lower():
98
+ return "无效参数"
99
+ if "internal server error" in message.lower():
100
+ return "服务器内部错误"
101
+ if "service unavailable" in message.lower():
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}
hajimi.v0.0.4/app/utils/logging.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from datetime import datetime
3
+ from collections import deque
4
+ from threading import Lock
5
+
6
+ DEBUG = False # 可以从环境变量中获取
7
+
8
+ LOG_FORMAT_DEBUG = '%(asctime)s - %(levelname)s - [%(key)s]-%(request_type)s-[%(model)s]-%(status_code)s: %(message)s - %(error_message)s'
9
+ LOG_FORMAT_NORMAL = '[%(asctime)s] [%(levelname)s] [%(key)s]-%(request_type)s-[%(model)s]-%(status_code)s: %(message)s'
10
+
11
+ # 配置 logger
12
+ logger = logging.getLogger("my_logger")
13
+ logger.setLevel(logging.DEBUG)
14
+
15
+ # 控制台处理器
16
+ console_handler = logging.StreamHandler()
17
+ console_formatter = logging.Formatter('%(message)s')
18
+ console_handler.setFormatter(console_formatter)
19
+ logger.addHandler(console_handler)
20
+
21
+ # 日志缓存,用于在网页上显示最近的日志
22
+ class LogManager:
23
+ def __init__(self, max_logs=100):
24
+ self.logs = deque(maxlen=max_logs) # 使用双端队列存储最近的日志
25
+ self.lock = Lock()
26
+
27
+ def add_log(self, log_entry):
28
+ with self.lock:
29
+ self.logs.append(log_entry)
30
+
31
+ def get_recent_logs(self, count=50):
32
+ with self.lock:
33
+ return list(self.logs)[-count:]
34
+
35
+ # 创建日志管理器实例
36
+ log_manager = LogManager()
37
+
38
+ def format_log_message(level, message, extra=None):
39
+ extra = extra or {}
40
+ log_values = {
41
+ 'asctime': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
42
+ 'levelname': level,
43
+ 'key': extra.get('key', 'N/A'),
44
+ 'request_type': extra.get('request_type', 'N/A'),
45
+ 'model': extra.get('model', 'N/A'),
46
+ 'status_code': extra.get('status_code', 'N/A'),
47
+ 'error_message': extra.get('error_message', ''),
48
+ 'message': message
49
+ }
50
+ log_format = LOG_FORMAT_DEBUG if DEBUG else LOG_FORMAT_NORMAL
51
+ formatted_log = log_format % log_values
52
+
53
+ # 将格式化后的日志添加到日志管理器
54
+ log_entry = {
55
+ 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
56
+ 'level': level,
57
+ 'key': extra.get('key', 'N/A'),
58
+ 'request_type': extra.get('request_type', 'N/A'),
59
+ 'model': extra.get('model', 'N/A'),
60
+ 'status_code': extra.get('status_code', 'N/A'),
61
+ 'message': message,
62
+ 'error_message': extra.get('error_message', ''),
63
+ 'formatted': formatted_log
64
+ }
65
+ log_manager.add_log(log_entry)
66
+
67
+ return formatted_log
68
+
69
+ def log(level: str, message: str, **extra):
70
+ """简化日志记录的统一函数"""
71
+ msg = format_log_message(level.upper(), message, extra=extra)
72
+ getattr(logger, level.lower())(msg)
hajimi.v0.0.4/app/utils/maintenance.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from apscheduler.schedulers.background import BackgroundScheduler
3
+ from app.utils.logging import log
4
+ from app.utils.stats import clean_expired_stats
5
+ from app.config import api_call_stats
6
+ from app.utils import check_version
7
+ def handle_exception(exc_type, exc_value, exc_traceback):
8
+ """
9
+ 全局异常处理函数
10
+
11
+ 处理未捕获的异常,并记录到日志中
12
+ """
13
+ if issubclass(exc_type, KeyboardInterrupt):
14
+ sys.excepthook(exc_type, exc_value, exc_traceback)
15
+ return
16
+ from app.utils.error_handling import translate_error
17
+ error_message = translate_error(str(exc_value))
18
+ log('error', f"未捕获的异常: {error_message}", status_code=500, error_message=error_message)
19
+
20
+ def schedule_cache_cleanup(response_cache_manager, active_requests_manager):
21
+ """
22
+ 设置定期清理缓存和活跃请求的定时任务
23
+ 顺便定时检查更新
24
+ Args:
25
+ response_cache_manager: 响应缓存管理器实例
26
+ active_requests_manager: 活跃请求管理器实例
27
+ """
28
+ scheduler = BackgroundScheduler()
29
+ scheduler.add_job(response_cache_manager.clean_expired, 'interval', minutes=1) # 每分钟清理过期缓存
30
+ scheduler.add_job(active_requests_manager.clean_completed, 'interval', seconds=30) # 每30秒清理已完成的活跃请求
31
+ scheduler.add_job(active_requests_manager.clean_long_running, 'interval', minutes=5, args=[300]) # 每5分钟清理运行超过5分钟的任务
32
+ scheduler.add_job(clean_expired_stats, 'interval', minutes=5,args=[api_call_stats]) # 每5分钟清理过期的统计数据
33
+ scheduler.add_job(check_version, 'interval', minutes=240) # 每4小时检查更新
34
+ scheduler.start()
35
+
36
+ return scheduler
hajimi.v0.0.4/app/utils/rate_limiting.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from threading import Lock
3
+ from fastapi import HTTPException, Request
4
+
5
+ rate_limit_data = {}
6
+ rate_limit_lock = Lock()
7
+
8
+ def protect_from_abuse(request: Request, max_requests_per_minute: int = 30, max_requests_per_day_per_ip: int = 600):
9
+ now = int(time.time())
10
+ minute = now // 60
11
+ day = now // (60 * 60 * 24)
12
+
13
+ minute_key = f"{request.url.path}:{minute}"
14
+ day_key = f"{request.client.host}:{day}"
15
+
16
+ with rate_limit_lock:
17
+ minute_count, minute_timestamp = rate_limit_data.get(
18
+ minute_key, (0, now))
19
+ if now - minute_timestamp >= 60:
20
+ minute_count = 0
21
+ minute_timestamp = now
22
+ minute_count += 1
23
+ rate_limit_data[minute_key] = (minute_count, minute_timestamp)
24
+
25
+ day_count, day_timestamp = rate_limit_data.get(day_key, (0, now))
26
+ if now - day_timestamp >= 86400:
27
+ day_count = 0
28
+ day_timestamp = now
29
+ day_count += 1
30
+ rate_limit_data[day_key] = (day_count, day_timestamp)
31
+
32
+ if minute_count > max_requests_per_minute:
33
+ raise HTTPException(status_code=429, detail={
34
+ "message": "Too many requests per minute", "limit": max_requests_per_minute})
35
+ if day_count > max_requests_per_day_per_ip:
36
+ raise HTTPException(status_code=429, detail={"message": "Too many requests per day from this IP", "limit": max_requests_per_day_per_ip})
hajimi.v0.0.4/app/utils/request.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import time
3
+ from typing import Dict, Any
4
+ from app.utils.logging import log
5
+
6
+ class ActiveRequestsManager:
7
+ """管理活跃API请求的类"""
8
+
9
+ def __init__(self, requests_pool: Dict[str, asyncio.Task] = None):
10
+ self.active_requests = requests_pool if requests_pool is not None else {} # 存储活跃请求
11
+
12
+ def add(self, key: str, task: asyncio.Task):
13
+ """添加新的活跃请求任务"""
14
+ task.creation_time = time.time() # 添加创建时间属性
15
+ self.active_requests[key] = task
16
+
17
+ def get(self, key: str):
18
+ """获取活跃请求任务"""
19
+ return self.active_requests.get(key)
20
+
21
+ def remove(self, key: str):
22
+ """移除活跃请求任务"""
23
+ if key in self.active_requests:
24
+ del self.active_requests[key]
25
+ return True
26
+ return False
27
+
28
+ def remove_by_prefix(self, prefix: str):
29
+ """移除所有以特定前缀开头的活跃请求任务"""
30
+ keys_to_remove = [k for k in self.active_requests.keys() if k.startswith(prefix)]
31
+ for key in keys_to_remove:
32
+ self.remove(key)
33
+ return len(keys_to_remove)
34
+
35
+ def clean_completed(self):
36
+ """清理所有已完成或已取消的任务"""
37
+ keys_to_remove = []
38
+
39
+ for key, task in self.active_requests.items():
40
+ if task.done() or task.cancelled():
41
+ keys_to_remove.append(key)
42
+
43
+ for key in keys_to_remove:
44
+ self.remove(key)
45
+
46
+ # if keys_to_remove:
47
+ # log('info', f"清理已完成请求任务: {len(keys_to_remove)}个", cleanup='active_requests')
48
+
49
+ def clean_long_running(self, max_age_seconds: int = 300):
50
+ """清理长时间运行的任务"""
51
+ now = time.time()
52
+ long_running_keys = []
53
+
54
+ for key, task in list(self.active_requests.items()):
55
+ if (hasattr(task, 'creation_time') and
56
+ task.creation_time < now - max_age_seconds and
57
+ not task.done() and not task.cancelled()):
58
+
59
+ long_running_keys.append(key)
60
+ task.cancel() # 取消长时间运行的任务
61
+
62
+ if long_running_keys:
63
+ log('warning', f"取消长时间运行的任务: {len(long_running_keys)}个", cleanup='long_running_tasks')
64
+
65
+ async def check_client_disconnect(http_request, current_api_key: str, request_type: str, model: str):
66
+ """检查客户端是否断开连接"""
67
+ while True:
68
+ if await http_request.is_disconnected():
69
+ extra_log = {'key': current_api_key[:8], 'request_type': request_type, 'model': model, 'error_message': '检测到客户端断开连接'}
70
+ log('info', "客户端连接已中断,等待API请求完成", extra=extra_log)
71
+ return True
72
+ await asyncio.sleep(0.5)
hajimi.v0.0.4/app/utils/response.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from fastapi import status
3
+ from fastapi.responses import JSONResponse
4
+
5
+ def create_chat_response(model: str, choices: list, id: str = None):
6
+ """创建标准响应对象的工厂函数"""
7
+ return {
8
+ "id": id or f"chatcmpl-{int(time.time()*1000)}",
9
+ "object": "chat.completion",
10
+ "created": int(time.time()),
11
+ "model": model,
12
+ "choices": choices,
13
+ "usage": {
14
+ "prompt_tokens": 0,
15
+ "completion_tokens": 0,
16
+ "total_tokens": 0
17
+ }
18
+ }
19
+
20
+ def create_error_response(model: str, error_message: str):
21
+ """创建错误响应对象的工厂函数"""
22
+ return create_chat_response(
23
+ model=model,
24
+ choices=[{
25
+ "index": 0,
26
+ "message": {
27
+ "role": "assistant",
28
+ "content": error_message
29
+ },
30
+ "finish_reason": "error"
31
+ }]
32
+ )
33
+
34
+ def create_response(chat_request, response_content):
35
+ """创建标准响应对象但不缓存"""
36
+ # 创建响应对象
37
+ return create_chat_response(
38
+ model=chat_request.model,
39
+ choices=[{
40
+ "index": 0,
41
+ "message": {
42
+ "role": "assistant",
43
+ "content": response_content.text
44
+ },
45
+ "finish_reason": "stop"
46
+ }]
47
+ )
48
+
49
+ def handle_exception(exc_type, exc_value, exc_traceback, translate_error, log):
50
+ """处理全局异常的函数"""
51
+ if issubclass(exc_type, KeyboardInterrupt):
52
+ # 对于KeyboardInterrupt,使用默认处理
53
+ import sys
54
+ sys.excepthook(exc_type, exc_value, exc_traceback)
55
+ return
56
+
57
+ # 对于其他异常,记录日志
58
+ error_message = translate_error(str(exc_value))
59
+ log('error', f"未捕获的异常: {error_message}", status_code=500, error_message=error_message)
hajimi.v0.0.4/app/utils/stats.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+ from app.utils.logging import log
3
+
4
+ def clean_expired_stats(api_call_stats):
5
+ """清理过期统计数据的函数"""
6
+ now = datetime.now()
7
+
8
+ # 清理24小时前的数据
9
+ # 清理总调用次数
10
+ for hour_key in list(api_call_stats['last_24h']['total'].keys()):
11
+ try:
12
+ hour_time = datetime.strptime(hour_key, '%Y-%m-%d %H:00')
13
+ if (now - hour_time).total_seconds() > 24 * 3600: # 超过24小时
14
+ del api_call_stats['last_24h']['total'][hour_key]
15
+ except ValueError:
16
+ # 如果键格式不正确,直接删除
17
+ del api_call_stats['last_24h']['total'][hour_key]
18
+
19
+ # 清理按端点分类的数据
20
+ for endpoint in list(api_call_stats['last_24h']['by_endpoint'].keys()):
21
+ if not isinstance(api_call_stats['last_24h']['by_endpoint'][endpoint], dict):
22
+ del api_call_stats['last_24h']['by_endpoint'][endpoint]
23
+ continue
24
+
25
+ for hour_key in list(api_call_stats['last_24h']['by_endpoint'][endpoint].keys()):
26
+ try:
27
+ hour_time = datetime.strptime(hour_key, '%Y-%m-%d %H:00')
28
+ if (now - hour_time).total_seconds() > 24 * 3600: # 超过24小时
29
+ del api_call_stats['last_24h']['by_endpoint'][endpoint][hour_key]
30
+ except ValueError:
31
+ # 如果键格式不正确,直接删除
32
+ del api_call_stats['last_24h']['by_endpoint'][endpoint][hour_key]
33
+
34
+ # 清理一小时前的小时统计数据
35
+ one_hour_ago = now - timedelta(hours=1)
36
+ # 清理总调用次数
37
+ for hour_key in list(api_call_stats['hourly']['total'].keys()):
38
+ try:
39
+ hour_time = datetime.strptime(hour_key, '%Y-%m-%d %H:00')
40
+ if hour_time < one_hour_ago:
41
+ del api_call_stats['hourly']['total'][hour_key]
42
+ except ValueError:
43
+ # 如果键格式不正确,直接删除
44
+ del api_call_stats['hourly']['total'][hour_key]
45
+
46
+ # 清理按端点分类的数据
47
+ for endpoint in list(api_call_stats['hourly']['by_endpoint'].keys()):
48
+ if not isinstance(api_call_stats['hourly']['by_endpoint'][endpoint], dict):
49
+ del api_call_stats['hourly']['by_endpoint'][endpoint]
50
+ continue
51
+
52
+ for hour_key in list(api_call_stats['hourly']['by_endpoint'][endpoint].keys()):
53
+ try:
54
+ hour_time = datetime.strptime(hour_key, '%Y-%m-%d %H:00')
55
+ if hour_time < one_hour_ago:
56
+ del api_call_stats['hourly']['by_endpoint'][endpoint][hour_key]
57
+ except ValueError:
58
+ # 如果键格式不正确,直接删除
59
+ del api_call_stats['hourly']['by_endpoint'][endpoint][hour_key]
60
+
61
+ # 清理一分钟前的分钟统计数据
62
+ one_minute_ago = now - timedelta(minutes=1)
63
+ # 清理总调用次数
64
+ for minute_key in list(api_call_stats['minute']['total'].keys()):
65
+ try:
66
+ minute_time = datetime.strptime(minute_key, '%Y-%m-%d %H:%M')
67
+ if minute_time < one_minute_ago:
68
+ del api_call_stats['minute']['total'][minute_key]
69
+ except ValueError:
70
+ # 如果键格式不正确,直接删除
71
+ del api_call_stats['minute']['total'][minute_key]
72
+
73
+ # 清理按端点分类的数据
74
+ for endpoint in list(api_call_stats['minute']['by_endpoint'].keys()):
75
+ if not isinstance(api_call_stats['minute']['by_endpoint'][endpoint], dict):
76
+ del api_call_stats['minute']['by_endpoint'][endpoint]
77
+ continue
78
+
79
+ for minute_key in list(api_call_stats['minute']['by_endpoint'][endpoint].keys()):
80
+ try:
81
+ minute_time = datetime.strptime(minute_key, '%Y-%m-%d %H:%M')
82
+ if minute_time < one_minute_ago:
83
+ del api_call_stats['minute']['by_endpoint'][endpoint][minute_key]
84
+ except ValueError:
85
+ # 如果键格式不正确,直接删除
86
+ del api_call_stats['minute']['by_endpoint'][endpoint][minute_key]
87
+
88
+ def update_api_call_stats(api_call_stats, endpoint=None):
89
+ """
90
+ 更新API调用统计的函数
91
+
92
+ 参数:
93
+ - api_call_stats: 统计数据字典
94
+ - endpoint: APIkey,为None则只更新总调用次数
95
+ """
96
+ now = datetime.now()
97
+ hour_key = now.strftime('%Y-%m-%d %H:00')
98
+ minute_key = now.strftime('%Y-%m-%d %H:%M')
99
+
100
+ # 检查并清理过期统计
101
+ clean_expired_stats(api_call_stats)
102
+
103
+ # 初始化总调用次数键(如果不存在)
104
+ if hour_key not in api_call_stats['last_24h']['total']:
105
+ api_call_stats['last_24h']['total'][hour_key] = 0
106
+ if hour_key not in api_call_stats['hourly']['total']:
107
+ api_call_stats['hourly']['total'][hour_key] = 0
108
+ if minute_key not in api_call_stats['minute']['total']:
109
+ api_call_stats['minute']['total'][minute_key] = 0
110
+
111
+ # 更新总调用次数统计
112
+ api_call_stats['last_24h']['total'][hour_key] += 1
113
+ api_call_stats['hourly']['total'][hour_key] += 1
114
+ api_call_stats['minute']['total'][minute_key] += 1
115
+
116
+ # 如果提供了端点,更新按端点分类的统计
117
+ if endpoint:
118
+ # 确保端点字典存在
119
+ if endpoint not in api_call_stats['last_24h']['by_endpoint']:
120
+ api_call_stats['last_24h']['by_endpoint'][endpoint] = {}
121
+ if endpoint not in api_call_stats['hourly']['by_endpoint']:
122
+ api_call_stats['hourly']['by_endpoint'][endpoint] = {}
123
+ if endpoint not in api_call_stats['minute']['by_endpoint']:
124
+ api_call_stats['minute']['by_endpoint'][endpoint] = {}
125
+
126
+ # 初始化端点特定的键(如果不存在)
127
+ if hour_key not in api_call_stats['last_24h']['by_endpoint'][endpoint]:
128
+ api_call_stats['last_24h']['by_endpoint'][endpoint][hour_key] = 0
129
+ if hour_key not in api_call_stats['hourly']['by_endpoint'][endpoint]:
130
+ api_call_stats['hourly']['by_endpoint'][endpoint][hour_key] = 0
131
+ if minute_key not in api_call_stats['minute']['by_endpoint'][endpoint]:
132
+ api_call_stats['minute']['by_endpoint'][endpoint][minute_key] = 0
133
+
134
+ # 更新端点特定的统计
135
+ api_call_stats['last_24h']['by_endpoint'][endpoint][hour_key] += 1
136
+ api_call_stats['hourly']['by_endpoint'][endpoint][hour_key] += 1
137
+ api_call_stats['minute']['by_endpoint'][endpoint][minute_key] += 1
138
+
139
+ # 计算总调用次数
140
+ total_24h = sum(api_call_stats['last_24h']['total'].values())
141
+ total_hourly = sum(api_call_stats['hourly']['total'].values())
142
+ total_minute = sum(api_call_stats['minute']['total'].values())
143
+
144
+ log_message = "API调用统计已更新: 24小时=%s, 1小时=%s, 1分钟=%s" % (
145
+ total_24h, total_hourly, total_minute
146
+ )
147
+
148
+ # 如果提供了端点,添加端点特定的统计信息
149
+ if endpoint:
150
+ endpoint_24h = sum(api_call_stats['last_24h']['by_endpoint'][endpoint].values())
151
+ endpoint_hourly = sum(api_call_stats['hourly']['by_endpoint'][endpoint].values())
152
+ endpoint_minute = sum(api_call_stats['minute']['by_endpoint'][endpoint].values())
153
+
154
+ log_message += " | 端点 '%s': 24小时=%s, 1小时=%s, 1分钟=%s" % (
155
+ endpoint[:8], endpoint_24h, endpoint_hourly, endpoint_minute
156
+ )
157
+
158
+ log('info', log_message)
hajimi.v0.0.4/app/utils/version.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import logging
3
+ from app.utils.logging import log
4
+ import app.config.settings as settings
5
+ async def check_version():
6
+ """
7
+ 检查应用程序版本更新
8
+
9
+ 从本地和远程获取版本信息,并比较版本号以确定是否有更新
10
+ """
11
+ # 导入全局变量
12
+ try:
13
+ # 读取本地版本
14
+ with open("./version.txt", "r") as f:
15
+ version_line = f.read().strip()
16
+ settings.local_version = version_line.split("=")[1] if "=" in version_line else "0.0.0"
17
+
18
+ # 获取远程版本
19
+ github_url = "https://raw.githubusercontent.com/wyeeeee/hajimi/refs/heads/main/version.txt"
20
+ response = requests.get(github_url, timeout=5)
21
+ if response.status_code == 200:
22
+ version_line = response.text.strip()
23
+ settings.remote_version = version_line.split("=")[1] if "=" in version_line else "0.0.0"
24
+
25
+ # 比较版本号
26
+ local_parts = [int(x) for x in settings.local_version.split(".")]
27
+ remote_parts = [int(x) for x in settings.remote_version.split(".")]
28
+
29
+ # 确保两个列表长度相同
30
+ while len(local_parts) < len(remote_parts):
31
+ local_parts.append(0)
32
+ while len(remote_parts) < len(local_parts):
33
+ remote_parts.append(0)
34
+
35
+ # 比较版本号
36
+ settings.has_update = False
37
+ for i in range(len(local_parts)):
38
+ if remote_parts[i] > local_parts[i]:
39
+ settings.has_update = True
40
+ break
41
+ elif remote_parts[i] < local_parts[i]:
42
+ break
43
+
44
+ log('info', f"版本检查: 本地版本 {settings.local_version}, 远程版本 {settings.remote_version}, 有更新: {settings.has_update}")
45
+ else:
46
+ log('warning', f"无法获取远程版本信息,HTTP状态码: {response.status_code}")
47
+ except Exception as e:
48
+ log('error', f"版本检查失败: {str(e)}")
49
+
50
+ return settings.local_version, settings.remote_version, settings.has_update
hajimi.v0.0.4/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ httpx
4
+ python-dotenv
5
+ requests
6
+ apscheduler
7
+ jinja2
hajimi.v0.0.4/version.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ version=0.0.4