martin98ks commited on
Commit
7af9a88
·
verified ·
1 Parent(s): 764ad60

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +11 -0
  2. docker-compose.yml +9 -0
  3. main.py +343 -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,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json, time, hmac, hashlib, base64, os, asyncio, uuid
2
+ from datetime import datetime
3
+ from typing import List, Optional, Union
4
+ import logging
5
+
6
+ import httpx
7
+ from fastapi import FastAPI, HTTPException, Request
8
+ from fastapi.responses import StreamingResponse
9
+ from pydantic import BaseModel
10
+ from fastapi.responses import HTMLResponse
11
+
12
+ # ---------- 日志配置 ----------
13
+ logging.basicConfig(
14
+ level=logging.INFO, # 生产环境建议 INFO,调试改 DEBUG
15
+ format="%(asctime)s | %(levelname)s | %(message)s",
16
+ datefmt="%H:%M:%S",
17
+ )
18
+ logger = logging.getLogger("gemini")
19
+
20
+ # ---------- 配置 ----------
21
+ SECURE_C_SES = os.getenv("SECURE_C_SES")
22
+ HOST_C_OSES = os.getenv("HOST_C_OSES")
23
+ CSESIDX = os.getenv("CSESIDX")
24
+ CONFIG_ID = os.getenv("CONFIG_ID")
25
+ PROXY = os.getenv("PROXY") or None
26
+ JWT_TTL = 270
27
+
28
+ # ---------- 硬编码模型列表 ----------
29
+ FIXED_MODELS = [
30
+ "gemini-2.5-flash",
31
+ "gemini-2.5-pro",
32
+ "gemini-3-pro"
33
+ ]
34
+
35
+ # ---------- 核心:请求头伪装 ----------
36
+ def get_common_headers(jwt: str) -> dict:
37
+ return {
38
+ "accept": "*/*",
39
+ "accept-encoding": "gzip, deflate, br, zstd",
40
+ "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
41
+ "authorization": f"Bearer {jwt}",
42
+ "content-type": "application/json",
43
+ "origin": "https://business.gemini.google",
44
+ "referer": "https://business.gemini.google/",
45
+ "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",
46
+ "x-server-timeout": "1800",
47
+ "sec-ch-ua": '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
48
+ "sec-ch-ua-mobile": "?0",
49
+ "sec-ch-ua-platform": '"Windows"',
50
+ "sec-fetch-dest": "empty",
51
+ "sec-fetch-mode": "cors",
52
+ "sec-fetch-site": "cross-site",
53
+ }
54
+
55
+ # ---------- 加密工具 ----------
56
+ def urlsafe_b64encode(data: bytes) -> str:
57
+ return base64.urlsafe_b64encode(data).decode().rstrip("=")
58
+
59
+ def kq_encode(s: str) -> str:
60
+ b = bytearray()
61
+ for ch in s:
62
+ v = ord(ch)
63
+ if v > 255:
64
+ b.append(v & 255)
65
+ b.append(v >> 8)
66
+ else:
67
+ b.append(v)
68
+ return urlsafe_b64encode(bytes(b))
69
+
70
+ def create_jwt(key_bytes: bytes, key_id: str, csesidx: str) -> str:
71
+ now = int(time.time())
72
+ header = {"alg": "HS256", "typ": "JWT", "kid": key_id}
73
+ payload = {
74
+ "iss": "https://business.gemini.google",
75
+ "aud": "https://biz-discoveryengine.googleapis.com",
76
+ "sub": f"csesidx/{csesidx}",
77
+ "iat": now,
78
+ "exp": now + 300,
79
+ "nbf": now,
80
+ }
81
+ header_b64 = kq_encode(json.dumps(header, separators=(",", ":")))
82
+ payload_b64 = kq_encode(json.dumps(payload, separators=(",", ":")))
83
+ message = f"{header_b64}.{payload_b64}"
84
+ sig = hmac.new(key_bytes, message.encode(), hashlib.sha256).digest()
85
+ return f"{message}.{urlsafe_b64encode(sig)}"
86
+
87
+ # ---------- JWT 管理 ----------
88
+ class JWTManager:
89
+ def __init__(self) -> None:
90
+ self.jwt: str = ""
91
+ self.expires: float = 0
92
+ self._lock = asyncio.Lock()
93
+
94
+ async def get(self) -> str:
95
+ async with self._lock:
96
+ if time.time() > self.expires:
97
+ await self._refresh()
98
+ return self.jwt
99
+
100
+ async def _refresh(self) -> None:
101
+ cookie = f"__Secure-C_SES={SECURE_C_SES}"
102
+ if HOST_C_OSES:
103
+ cookie += f"; __Host-C_OSES={HOST_C_OSES}"
104
+
105
+ logger.debug("🔑 正在刷新 JWT...")
106
+ async with httpx.AsyncClient(proxies=PROXY, verify=False, timeout=30) as cli:
107
+ r = await cli.get(
108
+ "https://business.gemini.google/auth/getoxsrf",
109
+ params={"csesidx": CSESIDX},
110
+ headers={
111
+ "cookie": cookie,
112
+ "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",
113
+ "referer": "https://business.gemini.google/"
114
+ },
115
+ )
116
+ if r.status_code != 200:
117
+ logger.error(f"❌ getoxsrf 失败: {r.status_code} {r.text}")
118
+ raise HTTPException(r.status_code, "getoxsrf failed")
119
+
120
+ txt = r.text[4:] if r.text.startswith(")]}'") else r.text
121
+ data = json.loads(txt)
122
+
123
+ key_bytes = base64.urlsafe_b64decode(data["xsrfToken"] + "==")
124
+ self.jwt = create_jwt(key_bytes, data["keyId"], CSESIDX)
125
+ self.expires = time.time() + JWT_TTL
126
+ logger.info(f"✅ JWT 刷新成功")
127
+
128
+ jwt_mgr = JWTManager()
129
+
130
+ # ---------- Session 管理 ----------
131
+ async def get_session() -> str:
132
+ jwt = await jwt_mgr.get()
133
+ headers = get_common_headers(jwt)
134
+ body = {
135
+ "configId": CONFIG_ID,
136
+ "additionalParams": {"token": "-"},
137
+ "createSessionRequest": {
138
+ "session": {"name": "", "displayName": ""}
139
+ }
140
+ }
141
+
142
+ logger.debug("🌐 正在创建 Session...")
143
+ async with httpx.AsyncClient(proxies=PROXY, verify=False, timeout=30) as cli:
144
+ r = await cli.post(
145
+ "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetCreateSession",
146
+ headers=headers,
147
+ json=body,
148
+ )
149
+ if r.status_code != 200:
150
+ logger.error(f"❌ createSession 失败: {r.status_code} {r.text}")
151
+ raise HTTPException(r.status_code, "createSession failed")
152
+ sess_name = r.json()["session"]["name"]
153
+ return sess_name
154
+
155
+ # ---------- OpenAI 兼容接口 ----------
156
+ app = FastAPI(title="Gemini-Business OpenAI Gateway")
157
+
158
+ class Message(BaseModel):
159
+ role: str
160
+ content: str
161
+
162
+ class ChatRequest(BaseModel):
163
+ model: str = FIXED_MODELS[0]
164
+ messages: List[Message]
165
+ stream: bool = False
166
+ temperature: Optional[float] = 0.7
167
+ top_p: Optional[float] = 1.0
168
+
169
+ def create_chunk(id: str, created: int, model: str, delta: dict, finish_reason: Union[str, None]) -> str:
170
+ chunk = {
171
+ "id": id,
172
+ "object": "chat.completion.chunk",
173
+ "created": created,
174
+ "model": model,
175
+ "choices": [{
176
+ "index": 0,
177
+ "delta": delta,
178
+ "finish_reason": finish_reason
179
+ }]
180
+ }
181
+ return json.dumps(chunk)
182
+ @app.get("/")
183
+ async def root():
184
+ """根路径返回详细的 API 信息"""
185
+ html_content = """
186
+ <html>
187
+ <head>
188
+ <title>Gemini Business API</title>
189
+ </head>
190
+ <body>
191
+ <h1>Gemini Business API 运行中</h1>
192
+ <p>可用的 API 端点:</p>
193
+ <ul>
194
+ <li><a href="/health">/health</a> - 健康检查</li>
195
+ <li><a href="/v1/models">/v1/models</a> - 模型列表</li>
196
+ <li><a href="/docs">/docs</a> - API 文档</li>
197
+ </ul>
198
+ <p>聊天接口: POST /v1/chat/completions</p>
199
+ </body>
200
+ </html>
201
+ """
202
+ return HTMLResponse(content=html_content)
203
+
204
+ @app.get("/v1/models")
205
+ async def list_models():
206
+ data = []
207
+ now = int(time.time())
208
+ for m in FIXED_MODELS:
209
+ data.append({
210
+ "id": m,
211
+ "object": "model",
212
+ "created": now,
213
+ "owned_by": "google",
214
+ "permission": []
215
+ })
216
+ return {"object": "list", "data": data}
217
+
218
+ @app.get("/health")
219
+ async def health():
220
+ return {"status": "ok", "time": datetime.utcnow().isoformat()}
221
+
222
+ @app.post("/v1/chat/completions")
223
+ async def chat(req: ChatRequest):
224
+ session = await get_session()
225
+ chat_id = f"chatcmpl-{uuid.uuid4()}"
226
+ created_time = int(time.time())
227
+
228
+ # --- 流式处理 ---
229
+ if req.stream:
230
+ return StreamingResponse(
231
+ stream_chat_generator(session, req, chat_id, created_time),
232
+ media_type="text/event-stream"
233
+ )
234
+
235
+ # --- 非流式处理 ---
236
+ full_content = ""
237
+ async for chunk_str in stream_chat_generator(session, req, chat_id, created_time, is_stream=False):
238
+ if chunk_str.startswith("data: [DONE]"):
239
+ break
240
+ if chunk_str.startswith("data: "):
241
+ try:
242
+ data = json.loads(chunk_str[6:])
243
+ delta = data["choices"][0]["delta"]
244
+ if "content" in delta:
245
+ full_content += delta["content"]
246
+ except:
247
+ pass
248
+
249
+ return {
250
+ "id": chat_id,
251
+ "object": "chat.completion",
252
+ "created": created_time,
253
+ "model": req.model,
254
+ "choices": [{
255
+ "index": 0,
256
+ "message": {
257
+ "role": "assistant",
258
+ "content": full_content
259
+ },
260
+ "finish_reason": "stop"
261
+ }],
262
+ "usage": {
263
+ "prompt_tokens": 0,
264
+ "completion_tokens": 0,
265
+ "total_tokens": 0
266
+ }
267
+ }
268
+
269
+ async def stream_chat_generator(session: str, req: ChatRequest, chat_id: str, created_time: int, is_stream: bool = True):
270
+ jwt = await jwt_mgr.get()
271
+ headers = get_common_headers(jwt)
272
+
273
+ body = {
274
+ "configId": CONFIG_ID,
275
+ "additionalParams": {"token": "-"},
276
+ "streamAssistRequest": {
277
+ "session": session,
278
+ "query": {"parts": [{"text": req.messages[-1].content}]},
279
+ "filter": "",
280
+ "fileIds": [],
281
+ "answerGenerationMode": "NORMAL",
282
+ "toolsSpec": {
283
+ "webGroundingSpec": {},
284
+ "toolRegistry": "default_tool_registry",
285
+ "imageGenerationSpec": {},
286
+ "videoGenerationSpec": {}
287
+ },
288
+ "languageCode": "zh-CN",
289
+ "userMetadata": {"timeZone": "Etc/GMT-8"},
290
+ "assistSkippingMode": "REQUEST_ASSIST"
291
+ }
292
+ }
293
+
294
+ # 1. 发送 Role
295
+ if is_stream:
296
+ chunk = create_chunk(chat_id, created_time, req.model, {"role": "assistant"}, None)
297
+ yield f"data: {chunk}\n\n"
298
+
299
+ # 2. 发起请求 (注意:这里不再使用 stream=True,而是等待完整响应)
300
+ # 这是为了解决 Google 返回 JSON 数组格式导致的解析错误
301
+ async with httpx.AsyncClient(proxies=PROXY, verify=False, timeout=120) as cli:
302
+ r = await cli.post(
303
+ "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetStreamAssist",
304
+ headers=headers,
305
+ json=body,
306
+ )
307
+
308
+ if r.status_code != 200:
309
+ logger.error(f"❌ Google 报错: {r.status_code} {r.text}")
310
+ if is_stream:
311
+ yield f"data: {json.dumps({'error': {'message': f'Upstream Error {r.status_code}'}})}\n\n"
312
+ return
313
+
314
+ try:
315
+ # 直接解析整个 JSON 数组
316
+ data_list = r.json()
317
+ except Exception as e:
318
+ logger.error(f"❌ JSON 解析失败: {e}")
319
+ if is_stream:
320
+ yield f"data: {json.dumps({'error': {'message': 'JSON Parse Error'}})}\n\n"
321
+ return
322
+
323
+ # 3. 遍历数组,模拟流式推送
324
+ for data in data_list:
325
+ for reply in data.get("streamAssistResponse", {}).get("answer", {}).get("replies", []):
326
+ text = reply.get("groundedContent", {}).get("content", {}).get("text", "")
327
+ if text and not reply.get("thought"):
328
+ chunk = create_chunk(chat_id, created_time, req.model, {"content": text}, None)
329
+ if is_stream:
330
+ yield f"data: {chunk}\n\n"
331
+
332
+ # 4. 发送 Finish Reason
333
+ if is_stream:
334
+ final_chunk = create_chunk(chat_id, created_time, req.model, {}, "stop")
335
+ yield f"data: {final_chunk}\n\n"
336
+ yield "data: [DONE]\n\n"
337
+
338
+ if __name__ == "__main__":
339
+ if not all([SECURE_C_SES, CSESIDX, CONFIG_ID]):
340
+ print("Error: Missing required environment variables.")
341
+ exit(1)
342
+ import uvicorn
343
+ 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