heixxin commited on
Commit
0f08815
·
verified ·
1 Parent(s): 178d225

Upload 5 files

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