tanbushi commited on
Commit
faaab61
·
1 Parent(s): 9b18d01
Files changed (4) hide show
  1. api_key_sb.py +42 -19
  2. app.py +1 -0
  3. docs/api_key_management_solution_v1.md +260 -0
  4. proxy.py +3 -1
api_key_sb.py CHANGED
@@ -20,30 +20,53 @@ def get_supabase_client() -> Client:
20
 
21
  supabase: Client = get_supabase_client()
22
 
23
- async def get_api_key_info(model: str = None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  try:
25
- # 根据用户提供的SQL语句修改,从 'airs_model_api_keys_view' 视图中获取 'api_key'
26
- if model:
27
- response = supabase.from_('airs_model_api_keys_view').select('api_key', 'api_key_id').eq('model_name', model).order('api_key_ran_at', desc=False).limit(1).execute()
28
- else:
29
- # 如果没有提供model,则获取所有模型的api_key
30
- raise HTTPException(status_code=400, detail="请提供模型名称!")
31
 
32
  if response.data:
33
  api_key_info = response.data[0]
 
 
 
 
 
34
  # 在返回api_key_info之前,更新其ran_at字段
35
- await update_api_key_ran_at(api_key_info.get('api_key_id'))
36
- return api_key_info
37
- else:
38
- logger.error(f"未找到模型 '{model}' 的API密钥信息。")
39
- raise HTTPException(status_code=404, detail=f"未找到模型 '{model}' 的API密钥信息。", headers={"X-Error-Type": "APIKeyNotFound"})
40
- except HTTPException as e:
41
- raise e # 重新抛出已有的HTTPException
42
  except Exception as e:
43
- logger.error(f"从Supabase获取API密钥失败: {e}")
44
- raise HTTPException(status_code=500, detail=f"从Supabase获取API密钥失败: {e}", headers={"X-Error-Type": "SupabaseFetchError"})
45
 
46
- async def update_api_key_ran_at(api_key_id: str, ran_at_time: datetime = None):
47
  """
48
  Updates the 'ran_at' field for a given API key in Supabase.
49
  If ran_at_time is None, it defaults to the current Beijing time.
@@ -58,11 +81,11 @@ async def update_api_key_ran_at(api_key_id: str, ran_at_time: datetime = None):
58
  current_local_time = ran_at_time.isoformat()
59
 
60
  logger.info(f"尝试更新 API 密钥 {api_key_id} 的 ran_at 为 {current_local_time}")
61
- response = supabase.table("airs_api_keys").update({"ran_at": current_local_time}).eq("id", api_key_id).execute()
62
  logger.info(f'API 密钥 {api_key_id} 的 ran_at 更新成功为 {current_local_time}. ')
63
 
64
  # 验证更新是否成功
65
- verification_response = supabase.from_('airs_api_keys').select('ran_at').eq('id', api_key_id).single().execute()
66
  verified_ran_at = verification_response.data.get('ran_at')
67
  logger.info(f"验证:API 密钥 {api_key_id} 的实际 ran_at 值为 {verified_ran_at}")
68
 
 
20
 
21
  supabase: Client = get_supabase_client()
22
 
23
+ # async def get_api_key_info(api_type_name: str, lookup_value: str):
24
+ # """
25
+ # 根据 API 类型名称和查找值从 Supabase 获取 API 密钥信息。
26
+ # """
27
+ # try:
28
+ # # 直接从 airs_model_api_keys_view_v1 视图中查询
29
+ # response = supabase.from_('airs_model_api_keys_view_v1').select('api_key, api_key_id').eq('api_type', api_type_name).eq('model_name', lookup_value).order('api_key_created_at', desc=False).limit(1).execute()
30
+
31
+ # if response.data:
32
+ # api_key_info = response.data[0]
33
+ # # 视图返回的 id 是 api_key_id,需要将其映射为 'id' 以保持与之前逻辑的兼容性
34
+ # formatted_api_key_info = {
35
+ # 'api_key': api_key_info.get('api_key'),
36
+ # 'id': api_key_info.get('api_key_id')
37
+ # }
38
+ # # 在返回api_key_info之前,更新其ran_at字段
39
+ # await update_api_key_ran_at(formatted_api_key_info.get('id'))
40
+ # return formatted_api_key_info
41
+ # return None
42
+ # except Exception as e:
43
+ # logger.error(f"Error fetching API key info from view: {e}")
44
+ # return None
45
+
46
+ async def get_api_key_info(lookup_value: str):
47
+ """
48
+ 根据 API 类型名称和查找值从 Supabase 获取 API 密钥信息。
49
+ """
50
  try:
51
+ # 直接从 airs_model_api_keys_view_v1 视图中查询
52
+ response = supabase.from_('airs_model_api_keys_view_v1').select('api_key, api_key_id').eq('model_name', lookup_value).order('api_key_created_at', desc=False).limit(1).execute()
 
 
 
 
53
 
54
  if response.data:
55
  api_key_info = response.data[0]
56
+ # 视图返回的 id 是 api_key_id,需要将其映射为 'id' 以保持与之前逻辑的兼容性
57
+ formatted_api_key_info = {
58
+ 'api_key': api_key_info.get('api_key'),
59
+ 'id': api_key_info.get('api_key_id')
60
+ }
61
  # 在返回api_key_info之前,更新其ran_at字段
62
+ await update_api_key_ran_at(formatted_api_key_info.get('id'))
63
+ return formatted_api_key_info
64
+ return None
 
 
 
 
65
  except Exception as e:
66
+ logger.error(f"Error fetching API key info from view: {e}")
67
+ return None
68
 
69
+ async def update_api_key_ran_at(api_key_id: int, ran_at_time: datetime = None):
70
  """
71
  Updates the 'ran_at' field for a given API key in Supabase.
72
  If ran_at_time is None, it defaults to the current Beijing time.
 
81
  current_local_time = ran_at_time.isoformat()
82
 
83
  logger.info(f"尝试更新 API 密钥 {api_key_id} 的 ran_at 为 {current_local_time}")
84
+ response = supabase.table("airs_api_keys_v1").update({"ran_at": current_local_time}).eq("id", api_key_id).execute()
85
  logger.info(f'API 密钥 {api_key_id} 的 ran_at 更新成功为 {current_local_time}. ')
86
 
87
  # 验证更新是否成功
88
+ verification_response = supabase.from_('airs_api_keys_v1').select('ran_at').eq('id', api_key_id).single().execute()
89
  verified_ran_at = verification_response.data.get('ran_at')
90
  logger.info(f"验证:API 密钥 {api_key_id} 的实际 ran_at 值为 {verified_ran_at}")
91
 
app.py CHANGED
@@ -54,6 +54,7 @@ async def health_check():
54
  # @app.api_route("/v1/{protocol}/{host}/{path:path}", methods=["POST"])
55
  async def proxy(request: Request, protocol: ProtocolType, host:str, path: str, proxy_api_key: str = Depends(verify_proxy_api_key)): # 添加代理认证依赖
56
  real_url = f"{protocol.value}://{host}/{path}"
 
57
  # 提取客户端请求的headers和body
58
  client_headers = dict(request.headers)
59
  client_body = await request.body()
 
54
  # @app.api_route("/v1/{protocol}/{host}/{path:path}", methods=["POST"])
55
  async def proxy(request: Request, protocol: ProtocolType, host:str, path: str, proxy_api_key: str = Depends(verify_proxy_api_key)): # 添加代理认证依赖
56
  real_url = f"{protocol.value}://{host}/{path}"
57
+
58
  # 提取客户端请求的headers和body
59
  client_headers = dict(request.headers)
60
  client_body = await request.body()
docs/api_key_management_solution_v1.md ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # API 密钥管理方案
2
+
3
+ 本文档概述了 `airs_api_keys` 表的数据库存储、查询和代码建议方案,以支持 LLM 和搜索引擎 API 密钥的灵活管理。
4
+
5
+ ## 1. `airs_api_keys` 表的原始结构
6
+
7
+ ```sql
8
+ create table public.airs_api_keys (
9
+ id bigint generated by default as identity not null,
10
+ created_at timestamp with time zone not null default now(),
11
+ api_key text null,
12
+ ran_at timestamp without time zone null default now(),
13
+ provider_id bigint null,
14
+ constraint airs_api_keys_pkey primary key (id),
15
+ constraint airs_api_keys_provider_id_fkey foreign KEY (provider_id) references airs_api_providers (id)
16
+ ) TABLESPACE pg_default;
17
+ ```
18
+
19
+ ## 2. `airs_api_keys` 表的建议修改结构
20
+
21
+ 为了支持不同类型的 API 密钥(LLM 和搜索引擎)以及更灵活的查找机制,建议修改 `airs_api_keys` 表结构如下:
22
+
23
+ ```sql
24
+ create table public.airs_api_keys (
25
+ id bigint generated by default as identity not null,
26
+ created_at timestamp with time zone not null default now(),
27
+ api_key text null,
28
+ ran_at timestamp without time zone null default now(),
29
+ provider_id bigint null,
30
+ api_type text not null, -- 'llm' or 'search'
31
+ lookup_key text not null, -- LLM 的模型名称 (e.g., 'glm-4', 'gemini-pro'), 搜索引擎的域名或特定路径 (e.g., 'api.tavily.com')
32
+ url_pattern text null, -- 可选: 用于验证或更精确匹配的 URL 模式 (e.g., 'https://open.bigmodel.cn/api/paas/v4/chat/completions')
33
+ constraint airs_api_keys_pkey primary key (id),
34
+ constraint airs_api_keys_provider_id_fkey foreign KEY (provider_id) references airs_api_providers (id),
35
+ constraint airs_api_keys_unique_lookup_key unique (api_type, lookup_key) -- 确保每种类型和查找键组合的唯一性
36
+ ) TABLESPACE pg_default;
37
+ ```
38
+
39
+ **字段说明:**
40
+
41
+ * `api_type`: 字符串类型,用于区分 API 密钥的类型,例如 `'llm'` 或 `'search'`。
42
+ * `lookup_key`: 字符串类型,作为查询 API 密钥的关键字段。
43
+ * 对于 LLM 类 API,这将是请求体中 `model` 字段的值(例如 `glm-4`, `gemini-pro`)。
44
+ * 对于搜索引擎类 API,这将是其域名或特定路径(例如 `api.tavily.com`)。
45
+ * `url_pattern`: 可选的字符串类型,用于存储与 API 密钥关联的完整或部分 URL 模式。这可以用于在某些情况下进行额外的验证或更精确的匹配。
46
+ * `constraint airs_api_keys_unique_lookup_key`: 确保 `api_type` 和 `lookup_key` 的组合是唯一的,防止重复的 API 密钥配置。
47
+
48
+ ### 示例数据库记录
49
+
50
+ 以下是一些 `airs_api_keys` 表中可能存在的示例记录:
51
+
52
+ | id | created_at | api_key | ran_at | provider_id | api_type | lookup_key | url_pattern |
53
+ | --- | ---------------------- | -------------------------- | ------------------- | ----------- | -------- | -------------- | ------------------------------------------------------------------------ |
54
+ | 1 | 2023-01-01 10:00:00+00 | sk-xxxxxxxxxxxxxxxxxxxxglm | 2023-01-01 10:00:00 | 1 | llm | glm-4 | https://open.bigmodel.cn/api/paas/v4/chat/completions |
55
+ | 2 | 2023-01-02 11:00:00+00 | AIzaSyBxxxxxxxxxxxxxxxxxx | 2023-01-02 11:00:00 | 2 | llm | gemini-pro | https://generativelanguage.googleapis.com/v1beta/openai/chat/completions |
56
+ | 3 | 2023-01-03 12:00:00+00 | tvly-xxxxxxxxxxxxxxxxxxxx | 2023-01-03 12:00:00 | 3 | search | api.tavily.com | https://api.tavily.com/search |
57
+
58
+ ## 3. 数据库查询方案
59
+
60
+ 根据修改后的表结构,可以按 `api_type` 和 `lookup_key` 高效地查询 API 密钥。
61
+
62
+ ### 查询 LLM API 密钥
63
+
64
+ 当代理服务收到 LLM 请求时,它会从请求体中提取 `model` 名称,并使用它作为 `lookup_key` 进行查询。
65
+
66
+ ```sql
67
+ SELECT api_key, provider_id
68
+ FROM public.airs_api_keys
69
+ WHERE api_type = 'llm' AND lookup_key = :model_name;
70
+ ```
71
+
72
+ * `:model_name` 将替换为从请求体中解析出的模型名称,例如 `'glm-4'` 或 `'gemini-pro'`。
73
+
74
+ ### 查询搜索引擎 API 密钥
75
+
76
+ 当代理服务收到搜索引擎请求时,它会根据请求的 URL 识别出对应的域名或特定路径,并使用它作为 `lookup_key` 进行查询。
77
+
78
+ ```sql
79
+ SELECT api_key, provider_id
80
+ FROM public.airs_api_keys
81
+ WHERE api_type = 'search' AND lookup_key = :domain_or_path;
82
+ ```
83
+
84
+ * `:domain_or_path` 将替换为从请求 URL 中提取的域名或路径,例如 `'api.tavily.com'`。
85
+
86
+ ## 4. 代码建议方案
87
+
88
+ 为了在 FastAPI 代理服务中实现 API 密钥的动态获取和注入,需要修改 `api_key_sb.py` 和 `proxy.py` 文件。
89
+
90
+ ### `api_key_sb.py` 中的 `get_api_key_info` 函数
91
+
92
+ 修改 `get_api_key_info` 函数以接受 `api_type` 和 `lookup_value` 作为参数,并根据这些参数从 Supabase 查询 API 密钥。
93
+
94
+ ```python
95
+ # api_key_sb.py
96
+
97
+ from supabase import create_client, Client
98
+ import os
99
+ from dotenv import load_dotenv
100
+
101
+ load_dotenv()
102
+
103
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
104
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
105
+
106
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
107
+
108
+ async def get_api_key_info(api_type: str, lookup_value: str):
109
+ """
110
+ 根据 API 类型和查找值从 Supabase 获取 API 密钥信息。
111
+ """
112
+ try:
113
+ response = supabase.from_('airs_api_keys').select('api_key, provider_id').eq('api_type', api_type).eq('lookup_key', lookup_value).limit(1).execute()
114
+ if response.data:
115
+ return response.data[0]
116
+ return None
117
+ except Exception as e:
118
+ print(f"Error fetching API key info: {e}")
119
+ return None
120
+ ```
121
+
122
+ ### `proxy.py` 中的请求处理逻辑
123
+
124
+ 在 `proxy.py` 中,需要修改请求处理逻辑以:
125
+
126
+ 1. 识别传入请求的 API 类型(LLM 或搜索引擎)。
127
+ 2. 根据 API 类型从请求中提取相应的 `lookup_value`(`model` 名称或域名/路径)。
128
+ 3. 调用 `get_api_key_info` 获取 API 密钥。
129
+ 4. 将获取到的 API 密钥注入到转发请求的相应位置(例如,请求头或请求体)。
130
+
131
+ 以下是 `proxy.py` 中 `handle_proxy_request` 函数的伪代码片段:
132
+
133
+ ```python
134
+ # proxy.py
135
+
136
+ from fastapi import FastAPI, Request, Response, HTTPException
137
+ from fastapi.responses import StreamingResponse
138
+ import httpx
139
+ import json
140
+ import logging
141
+ import re
142
+ from api_key_sb import get_api_key_info # 导入新的函数
143
+
144
+ # 配置日志
145
+ logging.basicConfig(level=logging.INFO)
146
+ logger = logging.getLogger(__name__)
147
+
148
+ app = FastAPI()
149
+ client = httpx.AsyncClient()
150
+
151
+ # 统一错误响应格式
152
+ def create_error_response(status_code: int, message: str):
153
+ return Response(
154
+ content=json.dumps({"error": {"message": message, "type": "proxy_error"}}),
155
+ status_code=status_code,
156
+ media_type="application/json"
157
+ )
158
+
159
+ @app.api_route("/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"])
160
+ async def handle_proxy_request(request: Request, path: str):
161
+ target_url_base = path # 假设 path 已经包含了目标 URL 的基础部分
162
+ api_key = None
163
+ api_type = None
164
+ lookup_value = None
165
+
166
+ # 识别 API 类型并提取 lookup_value
167
+ if "open.bigmodel.cn" in target_url_base or "generativelanguage.googleapis.com" in target_url_base:
168
+ api_type = "llm"
169
+ try:
170
+ request_body = await request.json()
171
+ lookup_value = request_body.get("model")
172
+ if not lookup_value:
173
+ logger.warning(f"LLM request missing 'model' in body for path: {path}")
174
+ return create_error_response(400, "LLM request body must contain 'model' field.")
175
+ except json.JSONDecodeError:
176
+ logger.warning(f"LLM request body is not valid JSON for path: {path}")
177
+ return create_error_response(400, "LLM request body must be valid JSON.")
178
+ elif "api.tavily.com" in target_url_base:
179
+ api_type = "search"
180
+ lookup_value = "api.tavily.com" # 或者从 URL 中更动态地提取,例如使用正则表达式
181
+ else:
182
+ logger.warning(f"Unknown API type for path: {path}")
183
+ return create_error_response(400, "Unknown API type or unsupported target URL.")
184
+
185
+ # 从数据库获取 API 密钥
186
+ if api_type and lookup_value:
187
+ api_key_info = await get_api_key_info(api_type, lookup_value)
188
+ if api_key_info:
189
+ api_key = api_key_info["api_key"]
190
+ logger.info(f"Successfully retrieved API key for type: {api_type}, lookup_value: {lookup_value}")
191
+ else:
192
+ logger.warning(f"API Key not found for type: {api_type}, lookup_value: {lookup_value}")
193
+ return create_error_response(401, "API Key not found for the requested service.")
194
+ else:
195
+ logger.error(f"Failed to determine API type or lookup value for path: {path}")
196
+ return create_error_response(500, "Internal server error: Could not determine API key parameters.")
197
+
198
+ # 构建目标 URL
199
+ # 假设原始请求的 path 已经包含了完整的后端服务 URL,例如 /v1/https/open.bigmodel.cn/api/paas/v4/chat/completions
200
+ # 我们需要提取出 https://open.bigmodel.cn/api/paas/v4/chat/completions
201
+ match = re.match(r"/v1/(https?://.*)", path)
202
+ if not match:
203
+ logger.error(f"Invalid target URL format in path: {path}")
204
+ return create_error_response(400, "Invalid target URL format.")
205
+
206
+ full_target_url = match.group(1)
207
+
208
+ # 准备转发请求的头部和内容
209
+ headers = dict(request.headers)
210
+ headers.pop("host", None) # 移除 host 头,避免后端服务解析错误
211
+ headers.pop("authorization", None) # 移除原始的 authorization 头,我们将注入新的 API 密钥
212
+
213
+ # 注入 API 密钥
214
+ if api_type == "llm":
215
+ # 对于 LLM,通常 API 密钥在 Authorization 头中
216
+ headers["Authorization"] = f"Bearer {api_key}"
217
+ elif api_type == "search":
218
+ # 对于 Tavily 等搜索引擎,API 密钥可能在请求体中或自定义头中
219
+ # 这里假设 Tavily API 密钥在请求体中,需要修改请求体
220
+ # 注意:修改请求体需要重新构建请求,这可能比较复杂
221
+ # 更简单的做法是如果后端支持,通过自定义头传递
222
+ # 假设 Tavily API 密钥在请求体中,且请求体是 JSON
223
+ if request.method == "POST":
224
+ try:
225
+ request_body = await request.json()
226
+ request_body["api_key"] = api_key # 注入 Tavily API 密钥
227
+ request_content = json.dumps(request_body).encode("utf-8")
228
+ headers["Content-Type"] = "application/json"
229
+ except json.JSONDecodeError:
230
+ logger.error(f"Search request body is not valid JSON for path: {path}")
231
+ return create_error_response(400, "Search request body must be valid JSON.")
232
+ else:
233
+ # 如果是 GET 请求,API 密钥可能在查询参数中,这里需要根据实际情况调整
234
+ logger.warning(f"Search API key injection for GET request not implemented for path: {path}")
235
+ return create_error_response(501, "Search API key injection for GET requests is not yet supported.")
236
+
237
+ # 转发请求
238
+ try:
239
+ req = client.build_request(
240
+ method=request.method,
241
+ url=full_target_url,
242
+ headers=headers,
243
+ content=request_content if 'request_content' in locals() else await request.body(),
244
+ params=request.query_params,
245
+ timeout=30.0 # 设置超时时间
246
+ )
247
+
248
+ proxy_response = await client.send(req, stream=True)
249
+
250
+ return StreamingResponse(
251
+ proxy_response.aiter_bytes(),
252
+ status_code=proxy_response.status_code,
253
+ headers=proxy_response.headers
254
+ )
255
+ except httpx.RequestError as e:
256
+ logger.error(f"HTTPX Request Error for {full_target_url}: {e}")
257
+ return create_error_response(500, f"Proxy request failed: {e}")
258
+ except Exception as e:
259
+ logger.error(f"An unexpected error occurred: {e}")
260
+ return create_error_response(500, "An unexpected error occurred during proxying.")
proxy.py CHANGED
@@ -39,6 +39,7 @@ async def do_proxy(url: str, method: str, headers: dict, content: str, max_retri
39
  model_name = None
40
  try:
41
  content_json = json.loads(content)
 
42
  model_name = content_json.get('model')
43
  if model_name:
44
  logger.info(f"从内容中提取的模型名称: {model_name}")
@@ -48,7 +49,7 @@ async def do_proxy(url: str, method: str, headers: dict, content: str, max_retri
48
  logger.warning("内容不是有效的 JSON 格式,无法提取 'model' 键。")
49
  except Exception as e:
50
  logger.error(f"提取模型名称时发生错误: {str(e)}")
51
-
52
  api_key_info = await get_api_key_info(model_name)
53
  if not api_key_info:
54
  return _create_error_response(status.HTTP_500_INTERNAL_SERVER_ERROR, "无法获取API密钥信息", "APIKeyError")
@@ -108,5 +109,6 @@ async def do_proxy(url: str, method: str, headers: dict, content: str, max_retri
108
  return _create_error_response(e.status_code, e.detail, "HTTPException")
109
 
110
  except Exception as e:
 
111
  logger.error(f"🔥 致命错误: {type(e).__name__}: {str(e)}")
112
  return _create_error_response(status.HTTP_500_INTERNAL_SERVER_ERROR, f"内部服务器错误: {str(e)}", "InternalServerError")
 
39
  model_name = None
40
  try:
41
  content_json = json.loads(content)
42
+
43
  model_name = content_json.get('model')
44
  if model_name:
45
  logger.info(f"从内容中提取的模型名称: {model_name}")
 
49
  logger.warning("内容不是有效的 JSON 格式,无法提取 'model' 键。")
50
  except Exception as e:
51
  logger.error(f"提取模型名称时发生错误: {str(e)}")
52
+ # api_key_info = await get_api_key_info('chat', model_name)
53
  api_key_info = await get_api_key_info(model_name)
54
  if not api_key_info:
55
  return _create_error_response(status.HTTP_500_INTERNAL_SERVER_ERROR, "无法获取API密钥信息", "APIKeyError")
 
109
  return _create_error_response(e.status_code, e.detail, "HTTPException")
110
 
111
  except Exception as e:
112
+ print('请检查当前是否运行在本机开发环境')
113
  logger.error(f"🔥 致命错误: {type(e).__name__}: {str(e)}")
114
  return _create_error_response(status.HTTP_500_INTERNAL_SERVER_ERROR, f"内部服务器错误: {str(e)}", "InternalServerError")