knight577 commited on
Commit
7d43851
·
verified ·
1 Parent(s): 4955d9b

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +11 -0
  2. docker-compose.yml +9 -0
  3. main.py +484 -0
  4. requirements.txt +4 -0
Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ gcc \
6
+ && pip install --no-cache-dir -r requirements.txt \
7
+ && apt-get purge -y gcc \
8
+ && apt-get autoremove -y \
9
+ && rm -rf /var/lib/apt/lists/*
10
+ COPY main.py .
11
+ CMD ["python", "-u", "main.py"]
docker-compose.yml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ geminibusiness:
3
+ build: .
4
+ container_name: geminibusiness
5
+ restart: unless-stopped
6
+ ports:
7
+ - "3003:8000"
8
+ env_file: .env
9
+
main.py ADDED
@@ -0,0 +1,484 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json, time, hmac, hashlib, base64, os, asyncio, uuid, ssl, re
2
+ from datetime import datetime
3
+ from typing import List, Optional, Union, Dict, Any
4
+ import logging
5
+ import httpx
6
+ from fastapi import FastAPI, HTTPException, Request, Depends
7
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
8
+ from fastapi.responses import StreamingResponse, HTMLResponse
9
+ from pydantic import BaseModel
10
+
11
+ # ---------- 日志配置 ----------
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format="%(asctime)s | %(levelname)s | %(message)s",
15
+ datefmt="%H:%M:%S",
16
+ )
17
+ logger = logging.getLogger("gemini")
18
+
19
+ # ---------- 默认配置 (环境变量作为后备) ----------
20
+ ENV_SECURE_C_SES = os.getenv("SECURE_C_SES")
21
+ ENV_HOST_C_OSES = os.getenv("HOST_C_OSES")
22
+ ENV_CSESIDX = os.getenv("CSESIDX")
23
+ ENV_CONFIG_ID = os.getenv("CONFIG_ID")
24
+ PROXY = os.getenv("PROXY") or None
25
+ TIMEOUT_SECONDS = 600
26
+
27
+ # ---------- 模型映射配置 ----------
28
+ MODEL_MAPPING = {
29
+ "gemini-auto": None,
30
+ "gemini-2.5-flash": "gemini-2.5-flash",
31
+ "gemini-2.5-pro": "gemini-2.5-pro",
32
+ "gemini-3-pro-preview": "gemini-3-pro-preview"
33
+ }
34
+
35
+ # ---------- 全局 Session 缓存 ----------
36
+ SESSION_CACHE: Dict[str, dict] = {}
37
+
38
+ # ---------- HTTP 客户端 ----------
39
+ http_client = httpx.AsyncClient(
40
+ proxies=PROXY,
41
+ verify=False,
42
+ http2=False,
43
+ timeout=httpx.Timeout(TIMEOUT_SECONDS, connect=60.0),
44
+ limits=httpx.Limits(max_keepalive_connections=20, max_connections=50)
45
+ )
46
+
47
+ security = HTTPBearer()
48
+
49
+ # ---------- 凭证管理类 ----------
50
+ class UserCredentials:
51
+ def __init__(self, config_id, secure_c_ses, host_c_oses, csesidx):
52
+ self.config_id = config_id
53
+ self.secure_c_ses = secure_c_ses
54
+ self.host_c_oses = host_c_oses
55
+ self.csesidx = csesidx
56
+
57
+ def parse_credentials(auth: HTTPAuthorizationCredentials = Depends(security)) -> UserCredentials:
58
+ """
59
+ 解析 API Key。
60
+ 支持格式:
61
+ 1. CONFIG_ID#SECURE_C_SES#HOST_C_OSES#CSESIDX (推荐:全动态)
62
+ 2. CONFIG_ID#SECURE_C_SES#HOST_C_OSES (使用环境变量 CSESIDX)
63
+ 3. CONFIG_ID (使用全部环境变量)
64
+ """
65
+ token = auth.credentials
66
+ parts = token.split("#")
67
+
68
+ if len(parts) >= 4:
69
+ # 格式: CONFIG_ID#SECURE_C_SES#HOST_C_OSES#CSESIDX
70
+ return UserCredentials(parts[0], parts[1], parts[2], parts[3])
71
+ elif len(parts) == 3:
72
+ # 格式: CONFIG_ID#SECURE_C_SES#HOST_C_OSES (回退环境变量 CSESIDX)
73
+ if not ENV_CSESIDX:
74
+ logger.warning("Warning: Key missing CSESIDX and env CSESIDX is empty.")
75
+ return UserCredentials(parts[0], parts[1], parts[2], ENV_CSESIDX or "")
76
+ else:
77
+ # 格式: CONFIG_ID (全部回退环境变量)
78
+ if not (ENV_SECURE_C_SES and ENV_CSESIDX):
79
+ raise HTTPException(401, "Server env missing cookies/csesidx, please provide in API Key")
80
+ return UserCredentials(token, ENV_SECURE_C_SES, ENV_HOST_C_OSES, ENV_CSESIDX)
81
+
82
+ # ---------- 工具函数 ----------
83
+ def get_common_headers(jwt: str) -> dict:
84
+ return {
85
+ "accept": "*/*",
86
+ "accept-encoding": "gzip, deflate, br, zstd",
87
+ "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
88
+ "authorization": f"Bearer {jwt}",
89
+ "content-type": "application/json",
90
+ "origin": "https://business.gemini.google",
91
+ "referer": "https://business.gemini.google/",
92
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
93
+ "x-server-timeout": "1800",
94
+ "sec-ch-ua": '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
95
+ "sec-ch-ua-mobile": "?0",
96
+ "sec-ch-ua-platform": '"Windows"',
97
+ "sec-fetch-dest": "empty",
98
+ "sec-fetch-mode": "cors",
99
+ "sec-fetch-site": "cross-site",
100
+ }
101
+
102
+ def urlsafe_b64encode(data: bytes) -> str:
103
+ return base64.urlsafe_b64encode(data).decode().rstrip("=")
104
+
105
+ def kq_encode(s: str) -> str:
106
+ b = bytearray()
107
+ for ch in s:
108
+ v = ord(ch)
109
+ if v > 255:
110
+ b.append(v & 255)
111
+ b.append(v >> 8)
112
+ else:
113
+ b.append(v)
114
+ return urlsafe_b64encode(bytes(b))
115
+
116
+ def create_jwt(key_bytes: bytes, key_id: str, csesidx: str) -> str:
117
+ now = int(time.time())
118
+ header = {"alg": "HS256", "typ": "JWT", "kid": key_id}
119
+ payload = {
120
+ "iss": "https://business.gemini.google",
121
+ "aud": "https://biz-discoveryengine.googleapis.com",
122
+ "sub": f"csesidx/{csesidx}",
123
+ "iat": now,
124
+ "exp": now + 300,
125
+ "nbf": now,
126
+ }
127
+ # 驼峰命名,防止 markdown 转义导致的 SyntaxError
128
+ headerBase64 = kq_encode(json.dumps(header, separators=(",", ":")))
129
+ payloadBase64 = kq_encode(json.dumps(payload, separators=(",", ":")))
130
+ message = f"{headerBase64}.{payloadBase64}"
131
+
132
+ sig = hmac.new(key_bytes, message.encode(), hashlib.sha256).digest()
133
+ return f"{message}.{urlsafe_b64encode(sig)}"
134
+
135
+ # ---------- JWT 管理 (使用 creds.csesidx) ----------
136
+ async def fetch_jwt(creds: UserCredentials) -> str:
137
+ cookie = f"__Secure-C_SES={creds.secure_c_ses}"
138
+ if creds.host_c_oses:
139
+ cookie += f"; __Host-C_OSES={creds.host_c_oses}"
140
+
141
+ # 使用传入的 csesidx
142
+ r = await http_client.get(
143
+ "https://business.gemini.google/auth/getoxsrf",
144
+ params={"csesidx": creds.csesidx},
145
+ headers={
146
+ "cookie": cookie,
147
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
148
+ "referer": "https://business.gemini.google/"
149
+ },
150
+ )
151
+ if r.status_code != 200:
152
+ logger.error(f"❌ getoxsrf 失败: {r.status_code} {r.text[:100]}")
153
+ raise HTTPException(401, "Cookie expired or invalid")
154
+
155
+ txt = r.text[4:] if r.text.startswith(")]}'") else r.text
156
+ data = json.loads(txt)
157
+ key_bytes = base64.urlsafe_b64decode(data["xsrfToken"] + "==")
158
+
159
+ # 传递 csesidx 给 create_jwt
160
+ return create_jwt(key_bytes, data["keyId"], creds.csesidx)
161
+
162
+ # ---------- Session & File 管理 ----------
163
+ async def create_google_session(creds: UserCredentials) -> str:
164
+ jwt = await fetch_jwt(creds)
165
+ headers = get_common_headers(jwt)
166
+ body = {
167
+ "configId": creds.config_id,
168
+ "additionalParams": {"token": "-"},
169
+ "createSessionRequest": {
170
+ "session": {"name": "", "displayName": ""}
171
+ }
172
+ }
173
+
174
+ logger.debug("🌐 申请新 Session...")
175
+ r = await http_client.post(
176
+ "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetCreateSession",
177
+ headers=headers,
178
+ json=body,
179
+ )
180
+ if r.status_code != 200:
181
+ logger.error(f"❌ createSession 失败: {r.status_code} {r.text}")
182
+ raise HTTPException(r.status_code, "createSession failed")
183
+ sess_name = r.json()["session"]["name"]
184
+ return sess_name
185
+
186
+ async def upload_context_file(creds: UserCredentials, session_name: str, mime_type: str, base64_content: str) -> str:
187
+ jwt = await fetch_jwt(creds)
188
+ headers = get_common_headers(jwt)
189
+
190
+ ext = mime_type.split('/')[-1] if '/' in mime_type else "bin"
191
+ fileName = f"upload_{int(time.time())}_{uuid.uuid4().hex[:6]}.{ext}"
192
+ body = {
193
+ "configId": creds.config_id,
194
+ "additionalParams": {"token": "-"},
195
+ "addContextFileRequest": {
196
+ "name": session_name,
197
+ "fileName": fileName,
198
+ "mimeType": mime_type,
199
+ "fileContents": base64_content
200
+ }
201
+ }
202
+ r = await http_client.post(
203
+ "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetAddContextFile",
204
+ headers=headers,
205
+ json=body,
206
+ )
207
+ if r.status_code != 200:
208
+ logger.error(f"❌ 上传文件失败: {r.status_code} {r.text}")
209
+ raise HTTPException(r.status_code, f"Upload failed: {r.text}")
210
+
211
+ data = r.json()
212
+ return data.get("addContextFileResponse", {}).get("fileId")
213
+
214
+ # ---------- 消息处理逻辑 ----------
215
+ def get_conversation_key(messages: List[dict]) -> str:
216
+ if not messages: return "empty"
217
+ first_msg = messages[0].copy()
218
+ if isinstance(first_msg.get("content"), list):
219
+ text_part = "".join([x["text"] for x in first_msg["content"] if x["type"] == "text"])
220
+ first_msg["content"] = text_part
221
+ key_str = json.dumps(first_msg, sort_keys=True)
222
+ return hashlib.md5(key_str.encode()).hexdigest()
223
+
224
+ def parse_last_message(messages: List[Dict]):
225
+ if not messages: return "", []
226
+ last_msg = messages[-1]
227
+ content = last_msg.content
228
+ text_content = ""
229
+ images = []
230
+ if isinstance(content, str):
231
+ text_content = content
232
+ elif isinstance(content, list):
233
+ for part in content:
234
+ if part.get("type") == "text":
235
+ text_content += part.get("text", "")
236
+ elif part.get("type") == "image_url":
237
+ url = part.get("image_url", {}).get("url", "")
238
+ match = re.match(r"data:(image/[^;]+);base64,(.+)", url)
239
+ if match:
240
+ images.append({"mime": match.group(1), "data": match.group(2)})
241
+ return text_content, images
242
+
243
+ def build_full_context_text(messages: List[Dict]) -> str:
244
+ prompt = ""
245
+ for msg in messages:
246
+ role = "User" if msg.role in ["user", "system"] else "Assistant"
247
+ contentStr = ""
248
+ if isinstance(msg.content, str):
249
+ contentStr = msg.content
250
+ elif isinstance(msg.content, list):
251
+ for part in msg.content:
252
+ if part.get("type") == "text":
253
+ contentStr += part.get("text", "")
254
+ elif part.get("type") == "image_url":
255
+ contentStr += "[图片]"
256
+ prompt += f"{role}: {contentStr}\n\n"
257
+ return prompt
258
+
259
+ # ---------- OpenAI 兼容接口 ----------
260
+ app = FastAPI(title="Gemini-Business OpenAI Gateway")
261
+
262
+ class Message(BaseModel):
263
+ role: str
264
+ content: Union[str, List[Dict[str, Any]]]
265
+
266
+ class ChatRequest(BaseModel):
267
+ model: str = "gemini-auto"
268
+ messages: List[Message]
269
+ stream: bool = False
270
+ temperature: Optional[float] = 0.7
271
+ top_p: Optional[float] = 1.0
272
+
273
+ def create_chunk(id: str, created: int, model: str, delta: dict, finish_reason: Union[str, None]) -> str:
274
+ chunk = {
275
+ "id": id,
276
+ "object": "chat.completion.chunk",
277
+ "created": created,
278
+ "model": model,
279
+ "choices": [{"index": 0, "delta": delta, "finish_reason": finish_reason}]
280
+ }
281
+ return json.dumps(chunk)
282
+
283
+ # [新增] 首页路由,防止访问域名报错 Not Found
284
+ @app.get("/", response_class=HTMLResponse)
285
+ async def root():
286
+ return """
287
+ <html>
288
+ <head>
289
+ <title>Gemini Business API</title>
290
+ <style>
291
+ body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f0f2f5; }
292
+ .container { text-align: center; padding: 2rem; background: white; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
293
+ h1 { color: #1a73e8; }
294
+ code { background: #eee; padding: 0.2rem 0.4rem; border-radius: 4px; }
295
+ </style>
296
+ </head>
297
+ <body>
298
+ <div class="container">
299
+ <h1>Gemini Business API is Running! 🚀</h1>
300
+ <p>Status: <b>Active</b></p>
301
+ <p>Chat Endpoint: <code>/v1/chat/completions</code></p>
302
+ <p>Models Endpoint: <code>/v1/models</code></p>
303
+ </div>
304
+ </body>
305
+ </html>
306
+ """
307
+
308
+ @app.get("/v1/models")
309
+ async def list_models():
310
+ models = []
311
+ for model_id in MODEL_MAPPING.keys():
312
+ models.append({
313
+ "id": model_id,
314
+ "object": "model",
315
+ "created": int(time.time()),
316
+ "owned_by": "google"
317
+ })
318
+ return {"object": "list", "data": models}
319
+
320
+ @app.post("/v1/chat/completions")
321
+ async def chat(req: ChatRequest, creds: UserCredentials = Depends(parse_credentials)):
322
+ if req.model not in MODEL_MAPPING:
323
+ raise HTTPException(status_code=404, detail=f"Model '{req.model}' not found.")
324
+ lastText, currentImages = parse_last_message(req.messages)
325
+ convKey = get_conversation_key([m.dict() for m in req.messages])
326
+ cached = SESSION_CACHE.get(convKey)
327
+
328
+ if cached:
329
+ googleSession = cached["session_id"]
330
+ textToSend = lastText
331
+ SESSION_CACHE[convKey]["updated_at"] = time.time()
332
+ isRetryMode = False
333
+ else:
334
+ googleSession = await create_google_session(creds)
335
+ textToSend = build_full_context_text(req.messages)
336
+ SESSION_CACHE[convKey] = {"session_id": googleSession, "updated_at": time.time()}
337
+ isRetryMode = True
338
+
339
+ chatId = f"chatcmpl-{uuid.uuid4()}"
340
+ createdTime = int(time.time())
341
+
342
+ async def response_wrapper():
343
+ retryCount = 0
344
+ maxRetries = 2
345
+ currentText = textToSend
346
+ currentRetryMode = isRetryMode
347
+ currentFileIds = []
348
+
349
+ while retryCount <= maxRetries:
350
+ try:
351
+ currentSession = SESSION_CACHE[convKey]["session_id"]
352
+
353
+ if currentImages and not currentFileIds:
354
+ for img in currentImages:
355
+ fid = await upload_context_file(creds, currentSession, img["mime"], img["data"])
356
+ currentFileIds.append(fid)
357
+
358
+ if currentRetryMode:
359
+ currentText = build_full_context_text(req.messages)
360
+
361
+ async for chunk in stream_chat_generator(
362
+ creds,
363
+ currentSession,
364
+ currentText,
365
+ currentFileIds,
366
+ req.model,
367
+ chatId,
368
+ createdTime,
369
+ req.stream
370
+ ):
371
+ yield chunk
372
+ break
373
+ except (httpx.ConnectError, httpx.ReadTimeout, ssl.SSLError, HTTPException) as e:
374
+ retryCount += 1
375
+ if retryCount <= maxRetries:
376
+ try:
377
+ newSess = await create_google_session(creds)
378
+ SESSION_CACHE[convKey] = {"session_id": newSess, "updated_at": time.time()}
379
+ currentRetryMode = True
380
+ currentFileIds = []
381
+ except Exception as create_err:
382
+ if req.stream: yield f"data: {json.dumps({'error': {'message': 'Recovery Failed'}})}\n\n"
383
+ return
384
+ else:
385
+ if req.stream: yield f"data: {json.dumps({'error': {'message': str(e)}})}\n\n"
386
+ return
387
+
388
+ if req.stream:
389
+ return StreamingResponse(response_wrapper(), media_type="text/event-stream")
390
+
391
+ fullContent = ""
392
+ async for chunk_str in response_wrapper():
393
+ if chunk_str.startswith("data: [DONE]"): break
394
+ if chunk_str.startswith("data: "):
395
+ try:
396
+ data = json.loads(chunk_str[6:])
397
+ delta = data["choices"][0]["delta"]
398
+ if "content" in delta: fullContent += delta["content"]
399
+ except: pass
400
+
401
+ return {
402
+ "id": chatId,
403
+ "object": "chat.completion",
404
+ "created": createdTime,
405
+ "model": req.model,
406
+ "choices": [{"index": 0, "message": {"role": "assistant", "content": fullContent}, "finish_reason": "stop"}],
407
+ "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
408
+ }
409
+
410
+ async def stream_chat_generator(creds: UserCredentials, session: str, text_content: str, file_ids: List[str], model_name: str, chat_id: str, created_time: int, is_stream: bool):
411
+ jwt = await fetch_jwt(creds)
412
+ headers = get_common_headers(jwt)
413
+
414
+ body = {
415
+ "configId": creds.config_id,
416
+ "additionalParams": {"token": "-"},
417
+ "streamAssistRequest": {
418
+ "session": session,
419
+ "query": {"parts": [{"text": text_content}]},
420
+ "fileIds": file_ids,
421
+ "answerGenerationMode": "NORMAL",
422
+ "toolsSpec": {"toolRegistry": "default_tool_registry"},
423
+ "languageCode": "zh-CN",
424
+ "userMetadata": {"timeZone": "Asia/Shanghai"},
425
+ "assistSkippingMode": "REQUEST_ASSIST"
426
+ }
427
+ }
428
+
429
+ target_model_id = MODEL_MAPPING.get(model_name)
430
+ if target_model_id:
431
+ body["streamAssistRequest"]["assistGenerationConfig"] = {"modelId": target_model_id}
432
+
433
+ if is_stream:
434
+ chunk = create_chunk(chat_id, created_time, model_name, {"role": "assistant"}, None)
435
+ yield f"data: {chunk}\n\n"
436
+
437
+ logger.info(f"📤 发送消息... Session: {session[-10:] if session else 'None'}")
438
+ r = await http_client.post(
439
+ "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetStreamAssist",
440
+ headers=headers,
441
+ json=body,
442
+ )
443
+
444
+ if r.status_code != 200:
445
+ logger.error(f"❌ HTTP错误: {r.status_code} {r.text}")
446
+ raise HTTPException(status_code=r.status_code, detail=f"Upstream Error {r.text}")
447
+
448
+ # === 调试打印 ===
449
+ log_text = r.text if len(r.text) < 1000 else r.text[:500] + "..."
450
+ logger.info(f"🔍 Google 返回内容: {log_text}")
451
+
452
+ try:
453
+ data_list = r.json()
454
+ except Exception:
455
+ logger.error("❌ JSON 解析失败")
456
+ raise HTTPException(status_code=502, detail="Invalid JSON response")
457
+
458
+ hasContent = False
459
+ for data in data_list:
460
+ if "error" in data:
461
+ logger.error(f"⚠️ 发现业务错误: {data['error']}")
462
+
463
+ for reply in data.get("streamAssistResponse", {}).get("answer", {}).get("replies", []):
464
+ text = reply.get("groundedContent", {}).get("content", {}).get("text", "")
465
+ if text:
466
+ hasContent = True
467
+ chunk = create_chunk(chat_id, created_time, model_name, {"content": text}, None)
468
+ if is_stream:
469
+ yield f"data: {chunk}\n\n"
470
+
471
+ if not hasContent:
472
+ # 显式报错给客户端
473
+ err_msg = "**[错误: Google 返回空响应。请检查 Logs 中的 'Google 返回内容',通常是 Cookie 失效或不匹配]**"
474
+ if is_stream:
475
+ yield f"data: {create_chunk(chat_id, created_time, model_name, {'content': err_msg}, None)}\n\n"
476
+
477
+ if is_stream:
478
+ final_chunk = create_chunk(chat_id, created_time, model_name, {}, "stop")
479
+ yield f"data: {final_chunk}\n\n"
480
+ yield "data: [DONE]\n\n"
481
+
482
+ if __name__ == "__main__":
483
+ import uvicorn
484
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi==0.110.0
2
+ uvicorn[standard]==0.29.0
3
+ httpx==0.27.0
4
+ pydantic==2.7.0