mingyang22 commited on
Commit
2cc5570
ยท
verified ยท
1 Parent(s): a977d0f

Upload folder using huggingface_hub

Browse files
Files changed (15) hide show
  1. .gitattributes +4 -0
  2. .gitignore +17 -0
  3. Dockerfile +22 -0
  4. LICENSE +21 -0
  5. README.md +198 -10
  6. a+b.png +3 -0
  7. a.png +3 -0
  8. api.py +206 -0
  9. b.png +3 -0
  10. client.py +1583 -0
  11. demo_chat.py +110 -0
  12. get_push_id.py +156 -0
  13. image.png +3 -0
  14. requirements.txt +4 -0
  15. server.py +1719 -0
.gitattributes CHANGED
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ a+b.png filter=lfs diff=lfs merge=lfs -text
37
+ a.png filter=lfs diff=lfs merge=lfs -text
38
+ b.png filter=lfs diff=lfs merge=lfs -text
39
+ image.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ๏ปฟ# Local config / secrets
2
+ config_data.json
3
+ *.local.json
4
+ *.secret
5
+ .env
6
+ .env.*
7
+
8
+ # Runtime logs
9
+ api_logs.json
10
+ *.log
11
+
12
+ # Python cache
13
+ __pycache__/
14
+ *.py[cod]
15
+
16
+ # Local media cache
17
+ media_cache/
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ # Ensure media_cache directory exists and is writable
11
+ RUN mkdir -p media_cache && chmod 777 media_cache
12
+
13
+ # Ensure config_data.json exists or is writable
14
+ # HF Spaces use a non-root user, so we need to make sure the app directory is writable
15
+ RUN chmod -R 777 /app
16
+
17
+ ENV PORT=7860
18
+ ENV PYTHONUNBUFFERED=1
19
+
20
+ EXPOSE 7860
21
+
22
+ CMD ["python", "server.py"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 erxiansheng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,10 +1,198 @@
1
- ---
2
- title: Gemini Open Relay
3
- emoji: ๐Ÿš€
4
- colorFrom: purple
5
- colorTo: green
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gemini Web ่ฝฌ OpenAI API
2
+
3
+ ๅŸบไบŽ Gemini ็ฝ‘้กต็‰ˆ็š„้€†ๅ‘ๅทฅ็จ‹๏ผŒๆไพ› OpenAI ๅ…ผๅฎน API ๆœๅŠกใ€‚
4
+
5
+ ## โœจ ๅŠŸ่ƒฝ็‰นๆ€ง
6
+
7
+ - โœ… ๆ–‡ๆœฌๅฏน่ฏ & ๅคš่ฝฎๅฏน่ฏ
8
+ - โœ… ๅ›พ็‰‡่ฏ†ๅˆซ๏ผˆๆ”ฏๆŒ base64 ๅ’Œ URL๏ผ‰
9
+ - โœ… ๅคšๅ›พ็‰‡ๆ”ฏๆŒ
10
+ - โœ… ๅ›พ็‰‡็”Ÿๆˆ๏ผˆ่‡ชๅŠจไธ‹่ฝฝ้ซ˜ๆธ…ๆ— ๆฐดๅฐๅŽŸๅ›พ๏ผ‰
11
+ - โœ… ่ง†้ข‘็”Ÿๆˆ๏ผˆๅผ‚ๆญฅ๏ผŒ้œ€ๅˆฐๅฎ˜็ฝ‘ๆŸฅ็œ‹๏ผ‰
12
+ - โœ… Token ่‡ชๅŠจๅˆทๆ–ฐ๏ผˆๅŽๅฐๅฎšๆ—ถๅˆทๆ–ฐ๏ผŒ้˜ฒๆญขๅคฑๆ•ˆ๏ผ‰
13
+ - โœ… Tools / Function Calling ๆ”ฏๆŒ
14
+ - โœ… OpenAI SDK ๅฎŒๅ…จๅ…ผๅฎน
15
+ - โœ… Web ๅŽๅฐ้…็ฝฎ็•Œ้ข
16
+
17
+ ## ๐Ÿš€ ๅฟซ้€Ÿๅผ€ๅง‹
18
+
19
+ ### 1. ๅฎ‰่ฃ…ไพ่ต–
20
+
21
+ ```bash
22
+ pip install -r requirements.txt
23
+ ```
24
+
25
+ ### 2. ๅฏๅŠจๆœๅŠก
26
+
27
+ ```bash
28
+ python server.py
29
+ ```
30
+
31
+ ### 3. ้…็ฝฎ Cookie
32
+
33
+ 1. ๆ‰“ๅผ€ๅŽๅฐ `http://localhost:8000/admin`๏ผˆ่ดฆๅท: admin / admin123๏ผ‰
34
+ 2. ็™ปๅฝ• [Gemini](https://gemini.google.com)๏ผŒF12 โ†’ Network โ†’ ๅคๅˆถ่ฏทๆฑ‚ๅคดไธญ็š„ Cookie
35
+ 3. ็ฒ˜่ดดๅˆฐๅŽๅฐ้…็ฝฎ้กต้ข๏ผŒไฟๅญ˜ๅณๅฏ
36
+
37
+ Cookie ่Žทๅ–็คบไพ‹๏ผš
38
+
39
+ ![Cookie่Žทๅ–็คบไพ‹](image.png)
40
+
41
+ ### 4. ่ฐƒ็”จ API
42
+
43
+ ```python
44
+ from openai import OpenAI
45
+
46
+ client = OpenAI(
47
+ base_url="http://localhost:8000/v1",
48
+ api_key="sk-geminixxxxx"
49
+ )
50
+
51
+ response = client.chat.completions.create(
52
+ model="gemini-3.0-flash",
53
+ messages=[{"role": "user", "content": "ไฝ ๅฅฝ"}]
54
+ )
55
+ print(response.choices[0].message.content)
56
+ ```
57
+
58
+ ## ๐Ÿ“ก API ไฟกๆฏ
59
+
60
+ | ้กน็›ฎ | ๅ€ผ |
61
+ |------|-----|
62
+ | Base URL | `http://localhost:8000/v1` |
63
+ | API Key | `sk-geminixxxxx` |
64
+ | ๅŽๅฐๅœฐๅ€ | `http://localhost:8000/admin` |
65
+
66
+ ### ๅฏ็”จๆจกๅž‹
67
+
68
+ - `gemini-3.0-flash` - ๅฟซ้€Ÿๅ“ๅบ”
69
+ - `gemini-3.0-flash-thinking` - ๆ€่€ƒๆจกๅผ
70
+ - `gemini-3.0-pro` - ไธ“ไธš็‰ˆ
71
+
72
+ ## ๐Ÿ’ฌ ไฝฟ็”จ็คบไพ‹
73
+
74
+ ### ๆ–‡ๆœฌๅฏน่ฏ
75
+
76
+ ```python
77
+ from openai import OpenAI
78
+
79
+ client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="sk-geminixxxxx")
80
+
81
+ response = client.chat.completions.create(
82
+ model="gemini-3.0-flash",
83
+ messages=[{"role": "user", "content": "ไฝ ๅฅฝ๏ผŒไป‹็ปไธ€ไธ‹ไฝ ่‡ชๅทฑ"}]
84
+ )
85
+ print(response.choices[0].message.content)
86
+ ```
87
+
88
+ ### ๅ•ๅ›พ็‰‡่ฏ†ๅˆซ
89
+
90
+ ```python
91
+ import base64
92
+ from openai import OpenAI
93
+
94
+ client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="sk-geminixxxxx")
95
+
96
+ def load_image_base64(path):
97
+ with open(path, "rb") as f:
98
+ return base64.b64encode(f.read()).decode()
99
+
100
+ img_b64 = load_image_base64("image.png")
101
+
102
+ response = client.chat.completions.create(
103
+ model="gemini-3.0-flash",
104
+ messages=[{
105
+ "role": "user",
106
+ "content": [
107
+ {"type": "text", "text": "ๆ่ฟฐ่ฟ™ๅผ ๅ›พ็‰‡"},
108
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_b64}"}}
109
+ ]
110
+ }]
111
+ )
112
+ print(response.choices[0].message.content)
113
+ ```
114
+
115
+ ### ๅคšๅ›พ็‰‡้—ฎ็ญ”
116
+
117
+ ![ๅคšๅ›พ้—ฎ็ญ”็คบๆ„](a+b.png)
118
+
119
+ ```python
120
+ import base64
121
+ from openai import OpenAI
122
+
123
+ client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="sk-geminixxxxx")
124
+
125
+ def load_image_base64(path):
126
+ with open(path, "rb") as f:
127
+ return base64.b64encode(f.read()).decode()
128
+
129
+ img1_b64 = load_image_base64("a.png")
130
+ img2_b64 = load_image_base64("b.png")
131
+
132
+ response = client.chat.completions.create(
133
+ model="gemini-3.0-pro",
134
+ messages=[{
135
+ "role": "user",
136
+ "content": [
137
+ {"type": "text", "text": "ๆŠŠ็ง‘ๆฏ”ๆ‰‹้‡Œ็š„็ƒคไธฒๆขๆˆๅฆๅค–ไธ€ๅผ ๅ›พ็š„ๆžช,ๅคš็”Ÿๆˆๅ‡ ๅผ "},
138
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img1_b64}"}},
139
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img2_b64}"}},
140
+ ]
141
+ }]
142
+ )
143
+ print(response.choices[0].message.content)
144
+ ```
145
+
146
+ ### ๅ›พ็‰‡็”Ÿๆˆ
147
+
148
+ ```python
149
+ from openai import OpenAI
150
+
151
+ client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="sk-geminixxxxx")
152
+
153
+ response = client.chat.completions.create(
154
+ model="gemini-3.0-pro",
155
+ messages=[{"role": "user", "content": "็”Ÿๆˆไธ€ๅผ ๅฏ็ˆฑ็š„็Œซๅ’ชๅ›พ็‰‡"}]
156
+ )
157
+ print(response.choices[0].message.content)
158
+ ```
159
+
160
+ ## ๐Ÿ”ง Token ็ฎก็†
161
+
162
+ ๅŽๅฐ้กต้ขๅทฆไธŠ่ง’ๆ˜พ็คบ Token ็Šถๆ€ๅ’Œๅˆทๆ–ฐๆฌกๆ•ฐใ€‚
163
+
164
+ API ็ซฏ็‚น๏ผš
165
+ - `GET /v1/token/status` - ๆŸฅ็œ‹็Šถๆ€
166
+ - `POST /v1/token/refresh` - ๆ‰‹ๅŠจๅˆทๆ–ฐ
167
+ - `POST /v1/client/reset` - ้‡็ฝฎๅฎขๆˆท็ซฏ
168
+
169
+ ## โš™๏ธ ้…็ฝฎ่ฏดๆ˜Ž
170
+
171
+ ็ผ–่พ‘ `server.py` ้กถ้ƒจ๏ผš
172
+
173
+ ```python
174
+ API_KEY = "sk-geminixxxxx" # API ๅฏ†้’ฅ
175
+ PORT = 8000 # ็ซฏๅฃ
176
+ ADMIN_USERNAME = "admin" # ๅŽๅฐ่ดฆๅท
177
+ ADMIN_PASSWORD = "admin123" # ๅŽๅฐๅฏ†็ 
178
+ TOKEN_REFRESH_INTERVAL_MIN = 200 # ๅˆทๆ–ฐ้—ด้š”ๆœ€ๅฐ็ง’ๆ•ฐ
179
+ TOKEN_REFRESH_INTERVAL_MAX = 300 # ๅˆทๆ–ฐ้—ด้š”ๆœ€ๅคง็ง’ๆ•ฐ
180
+ MEDIA_BASE_URL = "" # ๅช’ไฝ“ๅค–็ฝ‘ๅœฐๅ€๏ผŒๅฆ‚ https://your-domain.com
181
+ ```
182
+
183
+ ## ๐Ÿ“ ๆ–‡ไปถ่ฏด๏ฟฝ๏ฟฝ
184
+
185
+ | ๆ–‡ไปถ/ๆ–‡ไปถๅคน | ่ฏดๆ˜Ž |
186
+ |-------------|------|
187
+ | `server.py` | API ๆœๅŠก + Web ๅŽๅฐ |
188
+ | `client.py` | Gemini ้€†ๅ‘ๅฎขๆˆท็ซฏ |
189
+ | `demo_chat.py` | ๅฎŒๆ•ด่ฐƒ็”จ็คบไพ‹๏ผˆๆ–‡ๆœฌ/ๅ•ๅ›พ/ๅคšๅ›พ/็”Ÿๆˆ๏ผ‰ |
190
+ | `media_cache/` | AI ่ฟ”ๅ›žๅ›พ็‰‡็š„ไธญ่ฝฌ็ผ“ๅญ˜ๆ–‡ไปถๅคน |
191
+ | `image.png` | Cookie ่Žทๅ–็คบไพ‹ๅ›พ |
192
+ | `a.png` / `b.png` | ๅคšๅ›พ้—ฎ็ญ”็คบๆ„ๅ›พ |
193
+ | `requirements.txt` | Python ไพ่ต– |
194
+ | `config_data.json` | ่ฟ่กŒๆ—ถ้…็ฝฎ๏ผˆ่‡ชๅŠจ็”Ÿๆˆ๏ผ‰ |
195
+
196
+ ## ๐Ÿ“„ License
197
+
198
+ MIT
a+b.png ADDED

Git LFS Details

  • SHA256: b25322516859a358afb3142318dd1929bdb853188cdb6ecd84dff14a5b642ce9
  • Pointer size: 131 Bytes
  • Size of remote file: 261 kB
a.png ADDED

Git LFS Details

  • SHA256: 592295cc5030fc560cc48ab0428b91e684a793316ed4b1fa75a8e8ada62807ec
  • Pointer size: 131 Bytes
  • Size of remote file: 494 kB
api.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gemini OpenAI ๅ…ผๅฎน API
3
+
4
+ ๆไพ›ไธŽ OpenAI SDK ๅฎŒๅ…จๅ…ผๅฎน็š„ๆŽฅๅฃ๏ผŒๅฏ็›ดๆŽฅๆ›ฟๆข openai ๅบ“ไฝฟ็”จ
5
+
6
+ ไฝฟ็”จๆ–นๆณ•:
7
+ from api import GeminiOpenAI
8
+
9
+ client = GeminiOpenAI()
10
+ response = client.chat.completions.create(
11
+ model="gemini",
12
+ messages=[{"role": "user", "content": "ไฝ ๅฅฝ"}]
13
+ )
14
+ print(response.choices[0].message.content)
15
+ """
16
+
17
+ from client import GeminiClient, ChatCompletionResponse, Message, ChatCompletionChoice, Usage
18
+ from config import SECURE_1PSID, SNLM0E, COOKIES_STR, PUSH_ID
19
+ from typing import List, Dict, Any, Optional, Union
20
+ import base64
21
+ import time
22
+
23
+
24
+ class GeminiOpenAI:
25
+ """
26
+ OpenAI SDK ๅ…ผๅฎน็š„ Gemini ๅฎขๆˆท็ซฏ
27
+
28
+ ็”จๆณ•ไธŽ openai.OpenAI() ๅฎŒๅ…จไธ€่‡ด
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ cookies_str: str = None,
34
+ snlm0e: str = None,
35
+ push_id: str = None,
36
+ secure_1psid: str = None,
37
+ ):
38
+ """
39
+ ๅˆๅง‹ๅŒ–ๅฎขๆˆท็ซฏ
40
+
41
+ Args:
42
+ cookies_str: ๅฎŒๆ•ด cookie ๅญ—็ฌฆไธฒ๏ผˆๆŽจ่๏ผŒๅ›พ็‰‡ๅŠŸ่ƒฝๅฟ…้œ€๏ผ‰
43
+ snlm0e: AT Token๏ผˆๅฟ…ๅกซ๏ผ‰
44
+ push_id: ๅ›พ็‰‡ไธŠไผ  ID๏ผˆๅ›พ็‰‡ๅŠŸ่ƒฝๅฟ…้œ€๏ผ‰
45
+ secure_1psid: __Secure-1PSID cookie๏ผˆๅฆ‚ๆžœไธ็”จ cookies_str๏ผ‰
46
+ """
47
+ self._client = GeminiClient(
48
+ secure_1psid=secure_1psid or SECURE_1PSID,
49
+ snlm0e=snlm0e or SNLM0E,
50
+ cookies_str=cookies_str or COOKIES_STR,
51
+ push_id=push_id or PUSH_ID,
52
+ debug=False,
53
+ )
54
+ self.chat = self._Chat(self._client)
55
+
56
+ class _Chat:
57
+ def __init__(self, client: GeminiClient):
58
+ self._client = client
59
+ self.completions = self._Completions(client)
60
+
61
+ class _Completions:
62
+ def __init__(self, client: GeminiClient):
63
+ self._client = client
64
+
65
+ def create(
66
+ self,
67
+ model: str = "gemini",
68
+ messages: List[Dict[str, Any]] = None,
69
+ stream: bool = False,
70
+ **kwargs
71
+ ) -> ChatCompletionResponse:
72
+ """
73
+ ๅˆ›ๅปบ่ŠๅคฉๅฎŒๆˆ
74
+
75
+ Args:
76
+ model: ๆจกๅž‹ๅ็งฐ๏ผˆๅฟฝ็•ฅ๏ผŒๅง‹็ปˆไฝฟ็”จ Gemini๏ผ‰
77
+ messages: OpenAI ๆ ผๅผๆถˆๆฏๅˆ—่กจ
78
+ stream: ๆ˜ฏๅฆๆตๅผ่พ“ๅ‡บ๏ผˆๆš‚ไธๆ”ฏๆŒ๏ผ‰
79
+ **kwargs: ๅ…ถไป–ๅ‚ๆ•ฐ๏ผˆๅฟฝ็•ฅ๏ผ‰
80
+
81
+ Returns:
82
+ ChatCompletionResponse: OpenAI ๆ ผๅผๅ“ๅบ”
83
+
84
+ Example:
85
+ # ็บฏๆ–‡ๆœฌ
86
+ response = client.chat.completions.create(
87
+ model="gemini",
88
+ messages=[{"role": "user", "content": "ไฝ ๅฅฝ"}]
89
+ )
90
+
91
+ # ๅธฆๅ›พ็‰‡
92
+ response = client.chat.completions.create(
93
+ model="gemini",
94
+ messages=[{
95
+ "role": "user",
96
+ "content": [
97
+ {"type": "text", "text": "่ฟ™ๆ˜ฏไป€ไนˆ๏ผŸ"},
98
+ {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}
99
+ ]
100
+ }]
101
+ )
102
+ """
103
+ if stream:
104
+ raise NotImplementedError("ๆตๅผ่พ“ๅ‡บๆš‚ไธๆ”ฏๆŒ")
105
+
106
+ return self._client.chat(messages=messages)
107
+
108
+ def reset(self):
109
+ """้‡็ฝฎไผš่ฏไธŠไธ‹ๆ–‡"""
110
+ self._client.reset()
111
+
112
+ def get_history(self) -> List[Dict]:
113
+ """่Žทๅ–ๆถˆๆฏๅކๅฒ"""
114
+ return self._client.get_history()
115
+
116
+
117
+ # ไพฟๆทๅ‡ฝๆ•ฐ
118
+ def create_client(
119
+ cookies_str: str = None,
120
+ snlm0e: str = None,
121
+ push_id: str = None,
122
+ ) -> GeminiOpenAI:
123
+ """
124
+ ๅˆ›ๅปบ Gemini ๅฎขๆˆท็ซฏ๏ผˆOpenAI ๅ…ผๅฎน๏ผ‰
125
+
126
+ Args:
127
+ cookies_str: ๅฎŒๆ•ด cookie ๅญ—็ฌฆไธฒ
128
+ snlm0e: AT Token
129
+ push_id: ๅ›พ็‰‡ไธŠไผ  ID
130
+
131
+ Returns:
132
+ GeminiOpenAI: OpenAI ๅ…ผๅฎนๅฎขๆˆท็ซฏ
133
+ """
134
+ return GeminiOpenAI(
135
+ cookies_str=cookies_str,
136
+ snlm0e=snlm0e,
137
+ push_id=push_id,
138
+ )
139
+
140
+
141
+ def chat(
142
+ message: str,
143
+ image: bytes = None,
144
+ image_path: str = None,
145
+ reset: bool = False,
146
+ ) -> str:
147
+ """
148
+ ๅฟซ้€Ÿ่Šๅคฉๅ‡ฝๆ•ฐ๏ผˆๅ•ไพ‹ๆจกๅผ๏ผ‰
149
+
150
+ Args:
151
+ message: ๆถˆๆฏๆ–‡ๆœฌ
152
+ image: ๅ›พ็‰‡ไบŒ่ฟ›ๅˆถๆ•ฐๆฎ
153
+ image_path: ๅ›พ็‰‡ๆ–‡ไปถ่ทฏๅพ„
154
+ reset: ๆ˜ฏๅฆ้‡็ฝฎไธŠไธ‹ๆ–‡
155
+
156
+ Returns:
157
+ str: AI ๅ›žๅคๆ–‡ๆœฌ
158
+
159
+ Example:
160
+ from api import chat
161
+
162
+ # ็บฏๆ–‡ๆœฌ
163
+ reply = chat("ไฝ ๅฅฝ")
164
+
165
+ # ๅธฆๅ›พ็‰‡
166
+ reply = chat("่ฟ™ๆ˜ฏไป€ไนˆ๏ผŸ", image_path="photo.jpg")
167
+
168
+ # ้‡็ฝฎไธŠไธ‹ๆ–‡
169
+ reply = chat("ๆ–ฐ่ฏ้ข˜", reset=True)
170
+ """
171
+ global _default_client
172
+
173
+ if '_default_client' not in globals() or _default_client is None:
174
+ _default_client = GeminiOpenAI()
175
+
176
+ if reset:
177
+ _default_client.reset()
178
+
179
+ # ๅค„็†ๅ›พ็‰‡
180
+ img_data = None
181
+ if image:
182
+ img_data = image
183
+ elif image_path:
184
+ with open(image_path, 'rb') as f:
185
+ img_data = f.read()
186
+
187
+ # ๆž„ๅปบๆถˆๆฏ
188
+ if img_data:
189
+ messages = [{
190
+ "role": "user",
191
+ "content": [
192
+ {"type": "text", "text": message},
193
+ {
194
+ "type": "image_url",
195
+ "image_url": {"url": f"data:image/jpeg;base64,{base64.b64encode(img_data).decode()}"}
196
+ }
197
+ ]
198
+ }]
199
+ else:
200
+ messages = [{"role": "user", "content": message}]
201
+
202
+ response = _default_client.chat.completions.create(messages=messages)
203
+ return response.choices[0].message.content
204
+
205
+
206
+ _default_client: GeminiOpenAI = None
b.png ADDED

Git LFS Details

  • SHA256: 015055396b12f3dd8bcab506c0d32ddcb5718c166445481b6af009f8afe1ccc8
  • Pointer size: 131 Bytes
  • Size of remote file: 182 kB
client.py ADDED
@@ -0,0 +1,1583 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gemini Web Reverse Engineering Client
3
+ ๆ”ฏๆŒๅ›พๆ–‡่ฏทๆฑ‚ใ€ไธŠไธ‹ๆ–‡ๅฏน่ฏ๏ผŒOpenAI ๆ ผๅผ่พ“ๅ…ฅ่พ“ๅ‡บ
4
+ ๆ‰‹ๅŠจ้…็ฝฎ token๏ผŒๆ— ้œ€ไปฃ็ ็™ปๅฝ•
5
+ """
6
+
7
+ import re
8
+ import json
9
+ import random
10
+ import string
11
+ import base64
12
+ import uuid
13
+ import codecs
14
+ import httpx
15
+ from typing import Optional, List, Dict, Any, Union, Iterator
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime
18
+ import time
19
+
20
+
21
+ class CookieExpiredError(Exception):
22
+ """Cookie ่ฟ‡ๆœŸๆˆ–ๆ— ๆ•ˆๅผ‚ๅธธ"""
23
+ pass
24
+
25
+
26
+ class ImageUploadError(Exception):
27
+ """ๅ›พ็‰‡ไธŠไผ ๅคฑ่ดฅๅผ‚ๅธธ"""
28
+ pass
29
+
30
+
31
+ @dataclass
32
+ class Message:
33
+ """OpenAI ๆ ผๅผๆถˆๆฏ"""
34
+ role: str
35
+ content: Union[str, List[Dict[str, Any]]]
36
+
37
+
38
+ @dataclass
39
+ class ChatCompletionChoice:
40
+ index: int
41
+ message: Message
42
+ finish_reason: str = "stop"
43
+
44
+
45
+ @dataclass
46
+ class Usage:
47
+ prompt_tokens: int = 0
48
+ completion_tokens: int = 0
49
+ total_tokens: int = 0
50
+
51
+
52
+ @dataclass
53
+ class ChatCompletionResponse:
54
+ """OpenAI ๆ ผๅผๅ“ๅบ”"""
55
+ id: str
56
+ object: str = "chat.completion"
57
+ created: int = 0
58
+ model: str = "gemini-web"
59
+ choices: List[ChatCompletionChoice] = field(default_factory=list)
60
+ usage: Usage = field(default_factory=Usage)
61
+
62
+ def to_dict(self) -> dict:
63
+ return {
64
+ "id": self.id,
65
+ "object": self.object,
66
+ "created": self.created,
67
+ "model": self.model,
68
+ "choices": [
69
+ {
70
+ "index": c.index,
71
+ "message": {"role": c.message.role, "content": c.message.content},
72
+ "finish_reason": c.finish_reason
73
+ }
74
+ for c in self.choices
75
+ ],
76
+ "usage": {
77
+ "prompt_tokens": self.usage.prompt_tokens,
78
+ "completion_tokens": self.usage.completion_tokens,
79
+ "total_tokens": self.usage.total_tokens
80
+ }
81
+ }
82
+
83
+
84
+ class GeminiClient:
85
+ """
86
+ Gemini ็ฝ‘้กต็‰ˆ้€†ๅ‘ๅฎขๆˆท็ซฏ
87
+
88
+ ไฝฟ็”จๆ–นๆณ•:
89
+ 1. ๆ‰“ๅผ€ https://gemini.google.com ๅนถ็™ปๅฝ•
90
+ 2. F12 ๆ‰“ๅผ€ๅผ€ๅ‘่€…ๅทฅๅ…ท -> Application -> Cookies
91
+ 3. ๅคๅˆถไปฅไธ‹ cookie ๅ€ผ:
92
+ - __Secure-1PSID
93
+ - __Secure-1PSIDTS (ๅฏ้€‰)
94
+ 4. Network ๆ ‡็ญพ -> ๆ‰พไปปๆ„่ฏทๆฑ‚ -> ๅคๅˆถ SNlM0e ๅ€ผ (ๅœจ้กต้ขๆบ็ ไธญๆœ็ดข)
95
+ """
96
+
97
+ BASE_URL = "https://gemini.google.com"
98
+
99
+ def __init__(
100
+ self,
101
+ secure_1psid: str,
102
+ secure_1psidts: str = None,
103
+ secure_1psidcc: str = None,
104
+ snlm0e: str = None,
105
+ bl: str = None,
106
+ cookies_str: str = None,
107
+ push_id: str = None,
108
+ model_ids: dict = None,
109
+ debug: bool = False,
110
+ media_base_url: str = None,
111
+ ):
112
+ """
113
+ ๅˆๅง‹ๅŒ–ๅฎขๆˆท็ซฏ - ๆ‰‹ๅŠจๅกซๅ†™ token
114
+
115
+ Args:
116
+ secure_1psid: __Secure-1PSID cookie (ๅฟ…ๅกซ)
117
+ secure_1psidts: __Secure-1PSIDTS cookie (ๆŽจ่)
118
+ secure_1psidcc: __Secure-1PSIDCC cookie (ๆŽจ่)
119
+ snlm0e: SNlM0e token (ๅฟ…ๅกซ๏ผŒไปŽ้กต้ขๆบ็ ่Žทๅ–)
120
+ bl: BL ็‰ˆๆœฌๅท (ๅฏ้€‰๏ผŒ่‡ชๅŠจ่Žทๅ–)
121
+ cookies_str: ๅฎŒๆ•ด cookie ๅญ—็ฌฆไธฒ (ๅฏ้€‰๏ผŒๆ›ฟไปฃๅ•็‹ฌ่ฎพ็ฝฎ)
122
+ push_id: Push ID for image upload (ๅฟ…ๅกซ็”จไบŽๅ›พ็‰‡ไธŠไผ )
123
+ model_ids: ๆจกๅž‹ ID ๆ˜ ๅฐ„ {"flash": "xxx", "pro": "xxx", "thinking": "xxx"}
124
+ debug: ๆ˜ฏๅฆๆ‰“ๅฐ่ฐƒ่ฏ•ไฟกๆฏ
125
+ media_base_url: ๅช’ไฝ“ๆ–‡ไปถ็š„ๅŸบ็ก€ URL (ๅฆ‚ http://localhost:8000)๏ผŒ็”จไบŽๆž„ๅปบๅฎŒๆ•ด็š„ๅช’ไฝ“่ฎฟ้—ฎ URL
126
+ """
127
+ self.secure_1psid = secure_1psid
128
+ self.secure_1psidts = secure_1psidts
129
+ self.secure_1psidcc = secure_1psidcc
130
+ self.snlm0e = snlm0e
131
+ self.bl = bl
132
+ self.push_id = push_id
133
+ self.debug = debug
134
+ self.media_base_url = media_base_url or ""
135
+
136
+ # ๆจกๅž‹ ID ๆ˜ ๅฐ„ (็”จไบŽ่ฏทๆฑ‚ๅคด้€‰ๆ‹ฉๆจกๅž‹)
137
+ self.model_ids = model_ids or {
138
+ "flash": "56fdd199312815e2",
139
+ "pro": "e6fa609c3fa255c0",
140
+ "thinking": "e051ce1aa80aa576",
141
+ }
142
+
143
+ self.session = httpx.Client(
144
+ timeout=1220.0,
145
+ follow_redirects=True,
146
+ headers={
147
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
148
+ "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
149
+ "Origin": self.BASE_URL,
150
+ "Referer": f"{self.BASE_URL}/",
151
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
152
+ },
153
+ )
154
+
155
+ # ่ฎพ็ฝฎ cookies
156
+ if cookies_str:
157
+ self._set_cookies_from_string(cookies_str)
158
+ else:
159
+ self.session.cookies.set("__Secure-1PSID", secure_1psid, domain=".google.com")
160
+ if secure_1psidts:
161
+ self.session.cookies.set("__Secure-1PSIDTS", secure_1psidts, domain=".google.com")
162
+ if secure_1psidcc:
163
+ self.session.cookies.set("__Secure-1PSIDCC", secure_1psidcc, domain=".google.com")
164
+
165
+ # ไผš่ฏไธŠไธ‹ๆ–‡
166
+ self.conversation_id: str = ""
167
+ self.response_id: str = ""
168
+ self.choice_id: str = ""
169
+ self.request_count: int = 0
170
+
171
+ # ๆถˆๆฏๅކๅฒ
172
+ self.messages: List[Message] = []
173
+
174
+ # ้ชŒ่ฏๅฟ…ๅกซๅ‚ๆ•ฐ
175
+ if not self.snlm0e:
176
+ raise ValueError(
177
+ "SNlM0e ๆ˜ฏๅฟ…ๅกซๅ‚ๆ•ฐ๏ผ\n"
178
+ "่Žทๅ–ๆ–นๆณ•:\n"
179
+ "1. ๆ‰“ๅผ€ https://gemini.google.com ๅนถ็™ปๅฝ•\n"
180
+ "2. F12 -> ๆŸฅ็œ‹้กต้ขๆบไปฃ็  (Ctrl+U)\n"
181
+ "3. ๆœ็ดข 'SNlM0e' ๆ‰พๅˆฐ็ฑปไผผ: \"SNlM0e\":\"xxxxxx\"\n"
182
+ "4. ๅคๅˆถๅผ•ๅทๅ†…็š„ๅ€ผ"
183
+ )
184
+
185
+ # ่‡ชๅŠจ่Žทๅ– bl
186
+ if not self.bl:
187
+ self._fetch_bl()
188
+
189
+ def _set_cookies_from_string(self, cookies_str: str):
190
+ """ไปŽๅฎŒๆ•ด cookie ๅญ—็ฌฆไธฒ่งฃๆž"""
191
+ for item in cookies_str.split(";"):
192
+ item = item.strip()
193
+ if "=" in item:
194
+ key, value = item.split("=", 1)
195
+ self.session.cookies.set(key.strip(), value.strip(), domain=".google.com")
196
+
197
+ def _fetch_bl(self):
198
+ """่Žทๅ– BL ็‰ˆๆœฌๅท"""
199
+ try:
200
+ resp = self.session.get(self.BASE_URL)
201
+ match = re.search(r'"cfb2h":"([^"]+)"', resp.text)
202
+ if match:
203
+ self.bl = match.group(1)
204
+ else:
205
+ # ไฝฟ็”จ้ป˜่ฎคๅ€ผ
206
+ self.bl = "boq_assistant-bard-web-server_20241209.00_p0"
207
+ if self.debug:
208
+ print(f"[DEBUG] BL: {self.bl}")
209
+ except Exception as e:
210
+ self.bl = "boq_assistant-bard-web-server_20241209.00_p0"
211
+ if self.debug:
212
+ print(f"[DEBUG] ่Žทๅ– BL ๅคฑ่ดฅ๏ผŒไฝฟ็”จ้ป˜่ฎคๅ€ผ: {e}")
213
+
214
+ def refresh_tokens(self) -> dict:
215
+ """
216
+ ๅˆทๆ–ฐ token (SNlM0e ๅ’Œ push_id)
217
+
218
+ Returns:
219
+ dict: {"success": bool, "snlm0e": str, "push_id": str, "error": str}
220
+ """
221
+ result = {"success": False, "snlm0e": "", "push_id": "", "error": ""}
222
+
223
+ try:
224
+ if self.debug:
225
+ print("[DEBUG] ๅผ€ๅง‹ๅˆทๆ–ฐ token...")
226
+
227
+ # ่ฎฟ้—ฎ Gemini ้ฆ–้กตๅˆทๆ–ฐ session
228
+ resp = self.session.get(self.BASE_URL)
229
+
230
+ if resp.status_code != 200:
231
+ result["error"] = f"่ฎฟ้—ฎ Gemini ๅคฑ่ดฅ: HTTP {resp.status_code}"
232
+ return result
233
+
234
+ html = resp.text
235
+
236
+ # ๆๅ–ๆ–ฐ็š„ SNlM0e
237
+ snlm0e_patterns = [
238
+ r'"SNlM0e":"([^"]+)"',
239
+ r'SNlM0e["\s:]+["\']([^"\']+)["\']',
240
+ r'"at":"([^"]+)"',
241
+ ]
242
+ new_snlm0e = ""
243
+ for pattern in snlm0e_patterns:
244
+ match = re.search(pattern, html)
245
+ if match:
246
+ new_snlm0e = match.group(1)
247
+ break
248
+
249
+ if new_snlm0e:
250
+ self.snlm0e = new_snlm0e
251
+ result["snlm0e"] = new_snlm0e
252
+ if self.debug:
253
+ print(f"[DEBUG] SNlM0e ๅทฒๅˆทๆ–ฐ: {new_snlm0e[:30]}...")
254
+
255
+ # ๆๅ–ๆ–ฐ็š„ push_id
256
+ push_id_patterns = [
257
+ r'"push[_-]?id["\s:]+["\'](feeds/[a-z0-9]+)["\']',
258
+ r'push[_-]?id["\s:=]+["\'](feeds/[a-z0-9]+)["\']',
259
+ r'feedName["\s:]+["\'](feeds/[a-z0-9]+)["\']',
260
+ r'clientId["\s:]+["\'](feeds/[a-z0-9]+)["\']',
261
+ r'(feeds/[a-z0-9]{14,})',
262
+ ]
263
+ new_push_id = ""
264
+ for pattern in push_id_patterns:
265
+ matches = re.findall(pattern, html, re.IGNORECASE)
266
+ if matches:
267
+ new_push_id = matches[0]
268
+ break
269
+
270
+ if new_push_id:
271
+ self.push_id = new_push_id
272
+ result["push_id"] = new_push_id
273
+ if self.debug:
274
+ print(f"[DEBUG] push_id ๅทฒๅˆทๆ–ฐ: {new_push_id}")
275
+
276
+ # ๅŒๆ—ถๅˆทๆ–ฐ BL
277
+ match = re.search(r'"cfb2h":"([^"]+)"', html)
278
+ if match:
279
+ self.bl = match.group(1)
280
+
281
+ result["success"] = bool(new_snlm0e)
282
+ if not new_snlm0e:
283
+ result["error"] = "ๆ— ๆณ•ไปŽ้กต้ขๆๅ– SNlM0e๏ผŒCookie ๅฏ่ƒฝๅทฒๅฎŒๅ…จๅคฑๆ•ˆ"
284
+
285
+ return result
286
+
287
+ except Exception as e:
288
+ result["error"] = f"ๅˆทๆ–ฐ token ๅคฑ่ดฅ: {str(e)}"
289
+ if self.debug:
290
+ print(f"[DEBUG] ๅˆทๆ–ฐๅคฑ่ดฅ: {e}")
291
+ return result
292
+
293
+ def check_token_valid(self) -> bool:
294
+ """
295
+ ๆฃ€ๆŸฅๅฝ“ๅ‰ token ๆ˜ฏๅฆๆœ‰ๆ•ˆ
296
+
297
+ Returns:
298
+ bool: True ่กจ็คบๆœ‰ๆ•ˆ๏ผŒFalse ่กจ็คบ้œ€่ฆๅˆทๆ–ฐ
299
+ """
300
+ try:
301
+ resp = self.session.get(self.BASE_URL, timeout=10.0)
302
+ if resp.status_code != 200:
303
+ return False
304
+
305
+ # ๆฃ€ๆŸฅ้กต้ขๆ˜ฏๅฆๅŒ…ๅซ็™ปๅฝ•็Šถๆ€ๆ ‡่ฏ†
306
+ if 'SNlM0e' not in resp.text:
307
+ return False
308
+
309
+ return True
310
+ except Exception:
311
+ return False
312
+
313
+
314
+ def _parse_content(self, content: Union[str, List[Dict]]) -> tuple:
315
+ """่งฃๆž OpenAI ๆ ผๅผ content๏ผŒ่ฟ”ๅ›ž (text, images)"""
316
+ if isinstance(content, str):
317
+ return content, []
318
+
319
+ text_parts = []
320
+ images = []
321
+
322
+ for item in content:
323
+ if item.get("type") == "text":
324
+ text_parts.append(item.get("text", ""))
325
+ elif item.get("type") == "image_url":
326
+ # ๆ”ฏๆŒไธค็งๆ ผๅผ: {"url": "..."} ๆˆ–็›ดๆŽฅๅญ—็ฌฆไธฒ
327
+ image_url_data = item.get("image_url", {})
328
+ if isinstance(image_url_data, str):
329
+ url = image_url_data
330
+ else:
331
+ url = image_url_data.get("url", "")
332
+
333
+ if not url:
334
+ continue
335
+
336
+ if url.startswith("data:"):
337
+ # base64 ๆ ผๅผ: data:image/png;base64,xxxxx
338
+ match = re.match(r'data:([^;]+);base64,(.+)', url)
339
+ if match:
340
+ images.append({"mime_type": match.group(1), "data": match.group(2)})
341
+ elif url.startswith("http://") or url.startswith("https://"):
342
+ # URL ๆ ผๅผ๏ผŒไธ‹่ฝฝๅ›พ็‰‡
343
+ try:
344
+ if self.debug:
345
+ print(f"[DEBUG] ๅฐ่ฏ•ไธ‹่ฝฝๅ›พ็‰‡ URL: {url[:100]}...")
346
+ resp = httpx.get(url, timeout=30, follow_redirects=True)
347
+ if resp.status_code == 200:
348
+ mime = resp.headers.get("content-type", "image/jpeg").split(";")[0]
349
+ images.append({"mime_type": mime, "data": base64.b64encode(resp.content).decode()})
350
+ if self.debug:
351
+ print(f"[DEBUG] ๅ›พ็‰‡ไธ‹่ฝฝๆˆๅŠŸ: {len(resp.content)} bytes, mime: {mime}")
352
+ else:
353
+ if self.debug:
354
+ print(f"[DEBUG] ๅ›พ็‰‡ไธ‹่ฝฝๅคฑ่ดฅ: HTTP {resp.status_code}")
355
+ except Exception as e:
356
+ if self.debug:
357
+ print(f"[DEBUG] ไธ‹่ฝฝๅ›พ็‰‡ๅคฑ่ดฅ: {e}")
358
+ else:
359
+ # ๅฏ่ƒฝๆ˜ฏ็บฏ base64 ๅญ—็ฌฆไธฒ (ๆฒกๆœ‰ data: ๅ‰็ผ€)
360
+ try:
361
+ # ๅฐ่ฏ•่งฃ็ ้ชŒ่ฏๆ˜ฏๅฆๆ˜ฏๆœ‰ๆ•ˆ base64
362
+ base64.b64decode(url[:100]) # ๅช้ชŒ่ฏๅ‰100ๅญ—็ฌฆ
363
+ images.append({"mime_type": "image/png", "data": url})
364
+ if self.debug:
365
+ print(f"[DEBUG] ๆฃ€ๆต‹ๅˆฐ็บฏ base64 ๅ›พ็‰‡ๆ•ฐๆฎ")
366
+ except:
367
+ if self.debug:
368
+ print(f"[DEBUG] ๆ— ๆณ•่ฏ†ๅˆซ็š„ๅ›พ็‰‡ๆ ผๅผ: {url[:50]}...")
369
+
370
+ return " ".join(text_parts) if text_parts else "", images
371
+
372
+ def _upload_image(self, image_data: bytes, mime_type: str = "image/jpeg") -> str:
373
+ """
374
+ ไธŠไผ ๅ›พ็‰‡ๅˆฐ Gemini ๆœๅŠกๅ™จ
375
+
376
+ Args:
377
+ image_data: ๅ›พ็‰‡ไบŒ่ฟ›ๅˆถๆ•ฐๆฎ
378
+ mime_type: ๅ›พ็‰‡ MIME ็ฑปๅž‹
379
+
380
+ Returns:
381
+ str: ไธŠไผ ๅŽ็š„ๅ›พ็‰‡่ทฏๅพ„๏ผˆๅธฆ token๏ผ‰
382
+ """
383
+ if not self.push_id:
384
+ raise CookieExpiredError(
385
+ "ๅ›พ็‰‡ไธŠไผ ้œ€่ฆ push_id\n"
386
+ "่Žทๅ–ๆ–นๆณ•: ่ฟ่กŒ python get_push_id.py ๆˆ–ไปŽๆต่งˆๅ™จ Network ไธญ่Žทๅ–"
387
+ )
388
+
389
+ try:
390
+ upload_url = "https://push.clients6.google.com/upload/"
391
+ filename = f"image_{random.randint(100000, 999999)}.png"
392
+
393
+ # ๆต่งˆๅ™จๅฟ…้œ€็š„ๅคด
394
+ browser_headers = {
395
+ "accept": "*/*",
396
+ "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
397
+ "origin": "https://gemini.google.com",
398
+ "referer": "https://gemini.google.com/",
399
+ "sec-fetch-dest": "empty",
400
+ "sec-fetch-mode": "cors",
401
+ "sec-fetch-site": "same-site",
402
+ "x-browser-channel": "stable",
403
+ "x-browser-copyright": "Copyright 2025 Google LLC. All Rights reserved.",
404
+ "x-browser-validation": "Aj9fzfu+SaGLBY9Oqr3S7RokOtM=",
405
+ "x-browser-year": "2025",
406
+ "x-client-data": "CIa2yQEIpbbJAQipncoBCNvaygEIk6HLAQiFoM0BCJaMzwEIkZHPAQiSpM8BGOyFzwEYsobPAQ==",
407
+ }
408
+
409
+ # ็ฌฌไธ€ๆญฅ๏ผš่Žทๅ– upload_id
410
+ init_headers = {
411
+ **browser_headers,
412
+ "content-type": "application/x-www-form-urlencoded;charset=utf-8",
413
+ "push-id": self.push_id,
414
+ "x-goog-upload-command": "start",
415
+ "x-goog-upload-header-content-length": str(len(image_data)),
416
+ "x-goog-upload-protocol": "resumable",
417
+ "x-tenant-id": "bard-storage",
418
+ }
419
+
420
+ init_resp = self.session.post(upload_url, data={"File name": filename}, headers=init_headers)
421
+
422
+ if self.debug:
423
+ print(f"[DEBUG] ๅˆๅง‹ๅŒ–ไธŠไผ ็Šถๆ€: {init_resp.status_code}")
424
+
425
+ # ๆฃ€ๆŸฅๅˆๅง‹ๅŒ–ๅ“ๅบ”็Šถๆ€
426
+ if init_resp.status_code == 401 or init_resp.status_code == 403:
427
+ raise CookieExpiredError(
428
+ f"Cookie ๅทฒ่ฟ‡ๆœŸๆˆ–ๆ— ๆ•ˆ (HTTP {init_resp.status_code})\n"
429
+ "่ฏท้‡ๆ–ฐ่Žทๅ–ไปฅไธ‹ไฟกๆฏ:\n"
430
+ "1. __Secure-1PSID\n"
431
+ "2. __Secure-1PSIDTS\n"
432
+ "3. SNlM0e\n"
433
+ "4. push_id"
434
+ )
435
+
436
+ upload_id = init_resp.headers.get("x-guploader-uploadid")
437
+ if not upload_id:
438
+ raise CookieExpiredError(
439
+ f"ๆœช่Žทๅ–ๅˆฐ upload_id (็Šถๆ€็ : {init_resp.status_code})\n"
440
+ "ๅฏ่ƒฝๅŽŸๅ› : Cookie ๅทฒ่ฟ‡ๆœŸ๏ผŒ่ฏท้‡ๆ–ฐ่Žทๅ–ๆ‰€ๆœ‰ token"
441
+ )
442
+
443
+ if self.debug:
444
+ print(f"[DEBUG] Upload ID: {upload_id[:50]}...")
445
+
446
+ # ็ฌฌไบŒๆญฅ๏ผšไธŠไผ ๅ›พ็‰‡ๆ•ฐๆฎ
447
+ final_upload_url = f"{upload_url}?upload_id={upload_id}&upload_protocol=resumable"
448
+
449
+ upload_headers = {
450
+ **browser_headers,
451
+ "content-type": "application/x-www-form-urlencoded;charset=utf-8",
452
+ "push-id": self.push_id,
453
+ "x-goog-upload-command": "upload, finalize",
454
+ "x-goog-upload-offset": "0",
455
+ "x-tenant-id": "bard-storage",
456
+ "x-client-pctx": "CgcSBWjK7pYx",
457
+ }
458
+
459
+ upload_resp = self.session.post(
460
+ final_upload_url,
461
+ headers=upload_headers,
462
+ content=image_data
463
+ )
464
+
465
+ if self.debug:
466
+ print(f"[DEBUG] ไธŠไผ ๆ•ฐๆฎ็Šถๆ€: {upload_resp.status_code}")
467
+ print(f"[DEBUG] ๅ“ๅบ”ๅคด: {dict(upload_resp.headers)}")
468
+ print(f"[DEBUG] ๅ“ๅบ”ๅ†…ๅฎนๅฎŒๆ•ด: {upload_resp.text}")
469
+
470
+ # ๆฃ€ๆŸฅไธŠไผ ๅ“ๅบ”็Šถๆ€
471
+ if upload_resp.status_code == 401 or upload_resp.status_code == 403:
472
+ raise CookieExpiredError(
473
+ f"ไธŠไผ ๅ›พ็‰‡่ฎค่ฏๅคฑ่ดฅ (HTTP {upload_resp.status_code})\n"
474
+ "Cookie ๅทฒ่ฟ‡ๆœŸ๏ผŒ่ฏท้‡ๆ–ฐ่Žทๅ–"
475
+ )
476
+
477
+ if upload_resp.status_code != 200:
478
+ raise Exception(f"ไธŠไผ ๅ›พ็‰‡ๆ•ฐๆฎๅคฑ่ดฅ: {upload_resp.status_code}, ๅ“ๅบ”: {upload_resp.text[:200] if upload_resp.text else '(empty)'}")
479
+
480
+ # ไปŽๅ“ๅบ”ไธญๆๅ–ๅ›พ็‰‡่ทฏๅพ„
481
+ response_text = upload_resp.text
482
+ image_path = None
483
+
484
+ # ๅฐ่ฏ•่งฃๆž JSON
485
+ try:
486
+ response_json = json.loads(response_text)
487
+ image_path = self._extract_image_path(response_json)
488
+ except json.JSONDecodeError:
489
+ # ๅฆ‚ๆžœไธๆ˜ฏ JSON๏ผŒๅฐ่ฏ•ไปŽๆ–‡ๆœฌไธญๆๅ–่ทฏๅพ„
490
+ match = re.search(r'/contrib_service/[^\s"\']+', response_text)
491
+ if match:
492
+ image_path = match.group(0)
493
+
494
+ # ้ชŒ่ฏๅ›พ็‰‡่ทฏๅพ„ๅฎŒๆ•ดๆ€ง
495
+ if not image_path:
496
+ raise CookieExpiredError(
497
+ f"ๆ— ๆณ•ไปŽๅ“ๅบ”ไธญๆๅ–ๅ›พ็‰‡่ทฏๅพ„\n"
498
+ f"ๅ“ๅบ”ๅ†…ๅฎน: {response_text[:300]}\n"
499
+ "ๅฏ่ƒฝๅŽŸๅ› : Cookie ๅทฒ่ฟ‡ๆœŸ๏ผŒ่ฏท้‡ๆ–ฐ่Žทๅ–ๆ‰€ๆœ‰ token"
500
+ )
501
+
502
+ # ๆฃ€ๆŸฅ่ทฏๅพ„ๆ˜ฏๅฆๆœ‰ๆ•ˆ๏ผˆ้•ฟๅบฆ่ถณๅคŸๅณๅฏ๏ผŒๆ–ฐ็‰ˆๅฏ่ƒฝไธๅธฆๆŸฅ่ฏขๅ‚ๆ•ฐ๏ผ‰
503
+ if "/contrib_service/" in image_path:
504
+ # ่ทฏๅพ„้•ฟๅบฆ่‡ณๅฐ‘่ฆๆœ‰ไธ€ๅฎš้•ฟๅบฆๆ‰ๆ˜ฏๆœ‰ๆ•ˆ็š„
505
+ if len(image_path) < 40:
506
+ raise CookieExpiredError(
507
+ f"ๅ›พ็‰‡่ทฏๅพ„ไธๅฎŒๆ•ด\n"
508
+ f"่ฟ”ๅ›ž่ทฏๅพ„: {image_path}\n"
509
+ "ๅŽŸๅ› : Cookie ๅทฒ่ฟ‡ๆœŸๆˆ–ๆƒ้™ไธ่ถณ\n"
510
+ "่งฃๅ†ณๆ–นๆณ•:\n"
511
+ "1. ้‡ๆ–ฐ็™ปๅฝ• https://gemini.google.com\n"
512
+ "2. ๆ›ดๆ–ฐ config.py ไธญ็š„ๆ‰€ๆœ‰ token:\n"
513
+ " - SECURE_1PSID\n"
514
+ " - SECURE_1PSIDTS\n"
515
+ " - SNLM0E\n"
516
+ " - PUSH_ID"
517
+ )
518
+
519
+ if self.debug:
520
+ print(f"[DEBUG] ๅ›พ็‰‡่ทฏๅพ„: {image_path}")
521
+
522
+ return image_path
523
+
524
+ except CookieExpiredError:
525
+ raise
526
+ except Exception as e:
527
+ if self.debug:
528
+ print(f"[DEBUG] ไธŠไผ ๅคฑ่ดฅ: {e}")
529
+ raise Exception(f"ๅ›พ็‰‡ไธŠไผ ๅคฑ่ดฅ: {e}")
530
+
531
+ def _extract_image_path(self, data: Any) -> str:
532
+ """ไปŽๅ“ๅบ”ๆ•ฐๆฎไธญ้€’ๅฝ’ๆๅ–ๅ›พ็‰‡่ทฏๅพ„"""
533
+ if isinstance(data, str):
534
+ if data.startswith("/contrib_service/"):
535
+ return data
536
+ elif isinstance(data, dict):
537
+ for value in data.values():
538
+ result = self._extract_image_path(value)
539
+ if result:
540
+ return result
541
+ elif isinstance(data, list):
542
+ for item in data:
543
+ result = self._extract_image_path(item)
544
+ if result:
545
+ return result
546
+ return None
547
+
548
+ def _build_request_data(self, text: str, images: List[Dict] = None, image_paths: List[str] = None, model: str = None) -> str:
549
+ """ๆž„ๅปบ่ฏทๆฑ‚ๆ•ฐๆฎ - ๅŸบไบŽ็œŸๅฎž่ฏทๆฑ‚ๆ ผๅผ"""
550
+ # ไผš่ฏไธŠไธ‹ๆ–‡ (็ฉบๅญ—็ฌฆไธฒ่กจ็คบๆ–ฐๅฏน่ฏ)
551
+ conv_id = self.conversation_id or ""
552
+ resp_id = self.response_id or ""
553
+ choice_id = self.choice_id or ""
554
+
555
+ # ๅค„็†ๅ›พ็‰‡ๆ•ฐๆฎ - ๆ ผๅผ: [[[path, 1, null, mime_type], filename], ...]
556
+ image_data = None
557
+ if image_paths and len(image_paths) > 0:
558
+ image_data = []
559
+ for i, path in enumerate(image_paths):
560
+ mime_type = images[i]["mime_type"] if images and i < len(images) else "image/png"
561
+ filename = f"image_{random.randint(100000, 999999)}.png"
562
+ image_data.append([[path, 1, None, mime_type], filename])
563
+
564
+ # ็”Ÿๆˆๅ”ฏไธ€ไผš่ฏ ID
565
+ session_id = str(uuid.uuid4()).upper()
566
+ timestamp = int(time.time() * 1000)
567
+
568
+ # ๆจกๅž‹ๆ˜ ๅฐ„: ๅฐ†ๆจกๅž‹ๅ็งฐ่ฝฌๆขไธบ Gemini ๅ†…้ƒจๆจกๅž‹ๆ ‡่ฏ†
569
+ # [[0]] = gemini-3.0-pro (Pro ็‰ˆ)
570
+ # [[1]] = gemini-3.0-flash (ๅฟซ้€Ÿ็‰ˆ๏ผŒ้ป˜่ฎค)
571
+ # [[3]] = gemini-3.0-flash-thinking (ๆ€่€ƒ็‰ˆ)
572
+ model_code = [[1]] # ้ป˜่ฎคๅฟซ้€Ÿ็‰ˆ
573
+ if model:
574
+ model_lower = model.lower()
575
+ if "pro" in model_lower:
576
+ model_code = [[0]] # Pro ็‰ˆ
577
+ elif "thinking" in model_lower or "think" in model_lower:
578
+ model_code = [[3]] # ๆ€่€ƒ็‰ˆ
579
+ # flash ๆˆ–ๅ…ถไป–ๆƒ…ๅ†ตไฟๆŒ้ป˜่ฎค [[1]]
580
+
581
+ # ๆž„ๅปบๅ†…้ƒจ JSON ๆ•ฐ็ป„ (ๅŸบไบŽ็œŸๅฎž่ฏทๆฑ‚ๆ ผๅผ)
582
+ # ็ฌฌไธ€ไธชๅ…ƒ็ด : [text, 0, null, image_data, null, null, 0]
583
+ inner_data = [
584
+ [text, 0, None, image_data, None, None, 0],
585
+ ["zh-CN"],
586
+ [conv_id, resp_id, choice_id, None, None, None, None, None, None, ""],
587
+ self.snlm0e,
588
+ None, # ไน‹ๅ‰ๆ˜ฏ "test123"๏ผŒๆ”นไธบ null
589
+ None,
590
+ [1],
591
+ 1,
592
+ None,
593
+ None,
594
+ 1,
595
+ 0,
596
+ None,
597
+ None,
598
+ None,
599
+ None,
600
+ None,
601
+ model_code, # ๆจกๅž‹้€‰ๆ‹ฉๅญ—ๆฎต
602
+ 0,
603
+ None,
604
+ None,
605
+ None,
606
+ None,
607
+ None,
608
+ None,
609
+ None,
610
+ None,
611
+ 1,
612
+ None,
613
+ None,
614
+ [4],
615
+ None,
616
+ None,
617
+ None,
618
+ None,
619
+ None,
620
+ None,
621
+ None,
622
+ None,
623
+ None,
624
+ None,
625
+ [1],
626
+ None,
627
+ None,
628
+ None,
629
+ None,
630
+ None,
631
+ None,
632
+ None,
633
+ None,
634
+ None,
635
+ None,
636
+ None,
637
+ 0,
638
+ None,
639
+ None,
640
+ None,
641
+ None,
642
+ None,
643
+ session_id,
644
+ None,
645
+ [],
646
+ None,
647
+ None,
648
+ None,
649
+ None,
650
+ [timestamp // 1000, (timestamp % 1000) * 1000000]
651
+ ]
652
+
653
+ # ๅบๅˆ—ๅŒ–ไธบ JSON ๅญ—็ฌฆไธฒ
654
+ inner_json = json.dumps(inner_data, ensure_ascii=False, separators=(',', ':'))
655
+
656
+ # ๅค–ๅฑ‚ๅŒ…่ฃ…
657
+ outer_data = [None, inner_json]
658
+ f_req_value = json.dumps(outer_data, ensure_ascii=False, separators=(',', ':'))
659
+
660
+ return f_req_value
661
+
662
+
663
+ def _parse_response(self, response_text: str) -> str:
664
+ """่งฃๆžๅ“ๅบ”ๆ–‡ๆœฌ - ไฟฎๅค็‰ˆ"""
665
+ try:
666
+ # ่ทณ่ฟ‡ๅ‰็ผ€ๅนถๆŒ‰่กŒ่งฃๆž
667
+ lines = response_text.split("\n")
668
+ final_text = ""
669
+ generated_images_set = set() # ไฝฟ็”จ set ๅ…จๅฑ€ๅŽป้‡
670
+ last_inner_json = None # ไฟๅญ˜ๆœ€ๅŽไธ€ไธชๆœ‰ๆ•ˆ็š„ inner_json ็”จไบŽ่ฐƒ่ฏ•
671
+
672
+ for line in lines:
673
+ line = line.strip()
674
+ if not line or line.startswith(")]}'"):
675
+ continue
676
+
677
+ # ่ทณ่ฟ‡ๆ•ฐๅญ—่กŒ๏ผˆ้•ฟๅบฆๆ ‡่ฎฐ๏ผ‰
678
+ if line.isdigit():
679
+ continue
680
+
681
+ try:
682
+ data = json.loads(line)
683
+ # data ๆ˜ฏไธ€ไธชๅตŒๅฅ—ๆ•ฐ็ป„๏ผŒdata[0] ๆ‰ๆ˜ฏ็œŸๆญฃ็š„ๆ•ฐๆฎ
684
+ if isinstance(data, list) and len(data) > 0 and isinstance(data[0], list):
685
+ actual_data = data[0]
686
+ # ๆฃ€ๆŸฅๆ˜ฏๅฆๆ˜ฏ wrb.fr ๅ“ๅบ”
687
+ if len(actual_data) >= 3 and actual_data[0] == "wrb.fr" and actual_data[2]:
688
+ inner_json = json.loads(actual_data[2])
689
+ last_inner_json = inner_json
690
+
691
+ # ๅฐ่ฏ•ๆๅ–็”Ÿๆˆ็š„ๅ›พ็‰‡ URL๏ผŒๅˆๅนถๅˆฐๅ…จๅฑ€ set ไธญๅŽป้‡
692
+ imgs = self._extract_generated_images(inner_json)
693
+ if imgs:
694
+ for img in imgs:
695
+ generated_images_set.add(img)
696
+ if self.debug:
697
+ print(f"[DEBUG] ไปŽๅ“ๅบ”ไธญๆๅ–ๅˆฐ {len(imgs)} ไธชๅ›พ็‰‡ URL๏ผŒๅฝ“ๅ‰ๆ€ปๆ•ฐ: {len(generated_images_set)}")
698
+
699
+ # ๆๅ–ๆ–‡ๆœฌๅ†…ๅฎน
700
+ if inner_json and len(inner_json) > 4 and inner_json[4]:
701
+ candidates = inner_json[4]
702
+ if candidates and len(candidates) > 0:
703
+ candidate = candidates[0]
704
+ if candidate and len(candidate) > 1 and candidate[1]:
705
+ # candidate[1] ๆ˜ฏไธ€ไธชๆ•ฐ็ป„๏ผŒ็ฌฌไธ€ไธชๅ…ƒ็ด ๆ˜ฏๆ–‡ๆœฌ
706
+ text = candidate[1][0] if isinstance(candidate[1], list) else candidate[1]
707
+ if isinstance(text, str) and len(text) > len(final_text):
708
+ final_text = text
709
+ # ๆ›ดๆ–ฐไผš่ฏไธŠไธ‹ๆ–‡
710
+ if len(inner_json) > 1 and inner_json[1]:
711
+ if isinstance(inner_json[1], list):
712
+ if len(inner_json[1]) > 0:
713
+ self.conversation_id = inner_json[1][0] or self.conversation_id
714
+ if len(inner_json[1]) > 1:
715
+ self.response_id = inner_json[1][1] or self.response_id
716
+ if len(candidate) > 0:
717
+ self.choice_id = candidate[0] or self.choice_id
718
+ except Exception as e:
719
+ if self.debug:
720
+ print(f"[DEBUG] ่งฃๆž่กŒๆ—ถๅ‡บ้”™: {e}")
721
+ continue
722
+
723
+ # ่ฝฌๆขไธบๅˆ—่กจ
724
+ generated_images = list(generated_images_set)
725
+
726
+ if self.debug:
727
+ print(f"[DEBUG] ่งฃๆžๅฎŒๆˆ: final_text้•ฟๅบฆ={len(final_text)}, ๅ›พ็‰‡ๆ•ฐ้‡={len(generated_images)}")
728
+
729
+ # ๅค„็†็”Ÿๆˆ็š„ๅ›พ็‰‡/่ง†้ข‘ - ไธ‹่ฝฝๅนถ็ผ“ๅญ˜ๅˆฐๆœฌๅœฐ
730
+ if generated_images:
731
+ if self.debug:
732
+ print(f"[DEBUG] ๆๅ–ๅˆฐ {len(generated_images)} ไธชๅช’ไฝ“ URL๏ผŒๅผ€ๅง‹ไธ‹่ฝฝ...")
733
+
734
+ # ไธ‹่ฝฝๅ›พ็‰‡ๅนถ่Žทๅ–ๆœฌๅœฐไปฃ็† URL
735
+ local_media_urls = []
736
+ for i, url in enumerate(generated_images):
737
+ if self.debug:
738
+ print(f"[DEBUG] ไธ‹่ฝฝๅช’ไฝ“ {i+1}/{len(generated_images)}: {url[:80]}...")
739
+ local_url = self._download_media_as_data_url(url)
740
+ if local_url:
741
+ local_media_urls.append(local_url)
742
+ if self.debug:
743
+ print(f"[DEBUG] ๅช’ไฝ“ {i+1} ไธ‹่ฝฝๆˆๅŠŸ: {local_url}")
744
+ else:
745
+ # ไธ‹่ฝฝๅคฑ่ดฅ๏ผŒไฝฟ็”จๅŽŸๅง‹ URL
746
+ local_media_urls.append(url)
747
+ if self.debug:
748
+ print(f"[DEBUG] ๅช’ไฝ“ {i+1} ไธ‹่ฝฝๅคฑ่ดฅ๏ผŒไฝฟ็”จๅŽŸๅง‹ URL")
749
+
750
+ # ๆฃ€ๆต‹ๅ ไฝ็ฌฆ๏ผˆๅฆ‚ๆžœๆœ‰ๆ–‡ๆœฌ็š„่ฏ๏ผ‰
751
+ has_placeholder = False
752
+ if final_text:
753
+ has_placeholder = ('image_generation_content' in final_text or
754
+ 'video_gen_chip' in final_text)
755
+
756
+ # ๆž„ๅปบๅŒ…ๅซๆœฌๅœฐไปฃ็† URL ็š„ๅ“ๅบ”
757
+ media_parts = []
758
+ for i, url in enumerate(local_media_urls):
759
+ media_parts.append(f"![็”Ÿๆˆ็š„ๅ†…ๅฎน {i+1}]({url})")
760
+
761
+ media_text = "\n\n".join(media_parts)
762
+
763
+ if has_placeholder:
764
+ # ็งป้™คๅ ไฝ็ฌฆ URL
765
+ cleaned_text = re.sub(r'https?://googleusercontent\.com/(?:image_generation_content|video_gen_chip)/\d+', '', final_text)
766
+ cleaned_text = re.sub(r'http://googleusercontent\.com/(?:image_generation_content|video_gen_chip)/\d+', '', cleaned_text)
767
+ cleaned_text = re.sub(r'!\[.*?\]\(\)', '', cleaned_text) # ็งป้™ค็ฉบ็š„ๅ›พ็‰‡ๆ ‡่ฎฐ
768
+ cleaned_text = cleaned_text.strip()
769
+ if cleaned_text:
770
+ final_text = cleaned_text + "\n\n" + media_text
771
+ else:
772
+ final_text = media_text
773
+ elif final_text:
774
+ # ๆœ‰ๆ–‡ๆœฌไฝ†ๆฒกๆœ‰ๅ ไฝ็ฌฆ๏ผŒ่ฟฝๅŠ ๅ›พ็‰‡
775
+ final_text = final_text + "\n\n" + media_text
776
+ else:
777
+ # ๆฒกๆœ‰ๆ–‡ๆœฌ๏ผŒๅชๆœ‰ๅ›พ็‰‡
778
+ final_text = media_text
779
+
780
+ if self.debug:
781
+ print(f"[DEBUG] ๅช’ไฝ“ๅค„็†ๅฎŒๆˆ๏ผŒๆˆๅŠŸไธ‹่ฝฝ {len([u for u in local_media_urls if u.startswith('/media/')])} ไธช")
782
+
783
+ # ๆฃ€ๆต‹่ง†้ข‘็”Ÿๆˆๅ ไฝ็ฌฆ๏ผŒๆ›ฟๆขไธบๆ็คบๆ–‡ๆกˆ
784
+ is_video_generation = False
785
+ if final_text and 'video_gen_chip' in final_text:
786
+ is_video_generation = True
787
+
788
+ # ๆธ…็†ๆ–‡ๆœฌไธญ็š„ๅ ไฝ็ฌฆ URL ๅ’Œ็”จๆˆทไธŠไผ ๅ›พ็‰‡็š„ URL
789
+ if final_text:
790
+ # ๆธ…็†ๅ ไฝ็ฌฆ URL
791
+ final_text = re.sub(r'https?://googleusercontent\.com/(?:image_generation_content|video_gen_chip)/\d+\s*', '', final_text)
792
+ final_text = re.sub(r'http://googleusercontent\.com/(?:image_generation_content|video_gen_chip)/\d+\s*', '', final_text)
793
+ # ๆธ…็†็”จๆˆทไธŠไผ ๅ›พ็‰‡็š„ URL๏ผˆ/gg/ ่ทฏๅพ„๏ผŒ้ž /gg-dl/๏ผ‰
794
+ final_text = re.sub(r'!\[[^\]]*\]\(https://[^)]*googleusercontent\.com/gg/[^)]+\)', '', final_text)
795
+ final_text = re.sub(r'https://lh3\.googleusercontent\.com/gg/[^\s\)]+', '', final_text)
796
+ final_text = final_text.strip()
797
+
798
+ # ๅฆ‚ๆžœๆ˜ฏ่ง†้ข‘็”Ÿๆˆ๏ผŒๆทปๅŠ ๆ็คบๆ–‡ๆกˆ
799
+ if is_video_generation:
800
+ video_notice = "\n\n---\n๐Ÿ“น ่ง†้ข‘ไธบๅผ‚ๆญฅ็”Ÿๆˆ๏ผŒ็”Ÿๆˆ็ป“ๆžœๅฏๅœจๅฎ˜็ฝ‘่Šๅคฉ็ช—ๅฃๆŸฅ็œ‹ไธ‹่ฝฝใ€‚\n\nโฑ๏ธ ไฝฟ็”จ้™ๅˆถ๏ผš\n- ่ง†้ข‘็”Ÿๆˆ (Veo ๆจกๅž‹)๏ผšๆฏๅคฉๆ€ปๅ…ฑๅฏไปฅ็”Ÿๆˆ 3 ๆฌก\n- ๅ›พ็‰‡็”Ÿๆˆ (Nano Banana ๆจกๅž‹)๏ผšๆฏๅคฉๆ€ปๅ…ฑๅฏไปฅ็”Ÿๆˆ 1000 ๆฌก"
801
+ if final_text:
802
+ final_text = final_text + video_notice
803
+ else:
804
+ final_text = video_notice.strip()
805
+
806
+ if final_text:
807
+ # ไผ˜ๅŒ–ๅ›พ็‰‡ URL ไธบๅŽŸๅง‹้ซ˜ๆธ…ๅฐบๅฏธ๏ผˆไป…ๅฏนๆœชไธ‹่ฝฝ็š„ๅŽŸๅง‹ URL๏ผ‰
808
+ final_text = self._optimize_image_urls(final_text)
809
+ return final_text
810
+
811
+ # ๅฆ‚ๆžœๆฒกๆœ‰ๆ–‡ๆœฌไนŸๆฒกๆœ‰ๅ›พ็‰‡๏ผŒๅฐ่ฏ•ไปŽ last_inner_json ไธญๆๅ–ๆ›ดๅคšไฟกๆฏ
812
+ if self.debug and last_inner_json:
813
+ print(f"[DEBUG] ๆ— ๆณ•ๆๅ–ๅ†…ๅฎน๏ผŒinner_json ็ป“ๆž„: {str(last_inner_json)[:500]}...")
814
+
815
+ except Exception as e:
816
+ if self.debug:
817
+ print(f"[DEBUG] ่งฃๆž้”™่ฏฏ: {e}")
818
+
819
+ return "ๆ— ๆณ•่งฃๆžๅ“ๅบ”"
820
+
821
+ def _extract_generated_media(self, data: Any, depth: int = 0) -> List[str]:
822
+ """ไปŽๅ“ๅบ”ๆ•ฐๆฎไธญ้€’ๅฝ’ๆๅ–็”Ÿๆˆ็š„ๅ›พ็‰‡/่ง†้ข‘ URL
823
+
824
+ Gemini ไผš่ฟ”ๅ›žไธคไธชๅช’ไฝ“๏ผˆๅธฆๆฐดๅฐๅ’Œไธๅธฆๆฐดๅฐ๏ผ‰๏ผŒๆˆ‘ไปฌๅชไฟ็•™ๆœ€ๅŽไธ€ไธช๏ผˆไธๅธฆๆฐดๅฐ๏ผ‰
825
+ ๅชๆๅ– AI ็”Ÿๆˆ็š„ๅช’ไฝ“ (/gg-dl/ ่ทฏๅพ„)๏ผŒไธๆๅ–็”จๆˆทไธŠไผ ็š„ๅ›พ็‰‡ (/gg/ ่ทฏๅพ„)
826
+ """
827
+ if depth > 30: # ้˜ฒๆญขๆ— ้™้€’ๅฝ’
828
+ return []
829
+
830
+ media_urls = []
831
+
832
+ if isinstance(data, list):
833
+ # ๆฃ€ๆŸฅๆ˜ฏๅฆๆ˜ฏๅช’ไฝ“ๅฏน็ป“ๆž„: [[null, 1, "file1.png/mp4", "url1", ...], null, null, [null, 1, "file2.png/mp4", "url2", ...]]
834
+ # ็ฌฌไธ€ไธชๆ˜ฏๅธฆๆฐดๅฐ็š„๏ผŒ็ฌฌไบŒไธชๆ˜ฏไธๅธฆๆฐดๅฐ็š„
835
+ if (len(data) >= 1 and
836
+ isinstance(data[0], list) and len(data[0]) >= 4 and
837
+ data[0][0] is None and
838
+ isinstance(data[0][1], int) and
839
+ isinstance(data[0][2], str) and
840
+ isinstance(data[0][3], str) and
841
+ data[0][3].startswith('https://') and
842
+ 'gg-dl/' in data[0][3]): # ๅชๅŒน้… AI ็”Ÿๆˆ็š„ๅช’ไฝ“
843
+ # ๅฐ่ฏ•ๆ‰พ็ฌฌไบŒไธชๅช’ไฝ“๏ผˆไธๅธฆๆฐดๅฐ๏ผ‰
844
+ second_url = None
845
+ if len(data) >= 4 and isinstance(data[3], list) and len(data[3]) >= 4:
846
+ if (data[3][0] is None and
847
+ isinstance(data[3][3], str) and
848
+ 'gg-dl/' in data[3][3]):
849
+ second_url = data[3][3]
850
+
851
+ # ไผ˜ๅ…ˆไฝฟ็”จ็ฌฌไบŒไธช๏ผŒๅฆๅˆ™็”จ็ฌฌไธ€ไธช
852
+ url = second_url if second_url else data[0][3]
853
+ if 'image_generation_content' not in url and 'video_gen_chip' not in url:
854
+ media_urls.append(url)
855
+ return media_urls
856
+
857
+ # ๆฃ€ๆŸฅๆ˜ฏๅฆๆ˜ฏๅ•ไธชๅช’ไฝ“ๆ•ฐๆฎ็ป“ๆž„: [null, 1, "filename.png/mp4", "https://...gg-dl/..."]
858
+ if (len(data) >= 4 and
859
+ data[0] is None and
860
+ isinstance(data[1], int) and
861
+ isinstance(data[2], str) and
862
+ isinstance(data[3], str) and
863
+ data[3].startswith('https://') and
864
+ 'gg-dl/' in data[3]): # ๅชๅŒน้… AI ็”Ÿๆˆ็š„ๅช’ไฝ“
865
+ url = data[3]
866
+ if 'image_generation_content' not in url and 'video_gen_chip' not in url:
867
+ media_urls.append(url)
868
+ return media_urls
869
+
870
+ # ้€’ๅฝ’ๆœ็ดข๏ผŒๆ”ถ้›†ๆ‰€ๆœ‰ๅช’ไฝ“ URL
871
+ all_found = []
872
+ for item in data:
873
+ found = self._extract_generated_media(item, depth + 1)
874
+ if found:
875
+ all_found.extend(found)
876
+
877
+ # ๅฆ‚ๆžœๆ‰พๅˆฐๅคšไธช๏ผŒ่ฟ”ๅ›žๆœ€ๅŽไธ€ไธช๏ผˆ้€šๅธธๆ˜ฏไธๅธฆๆฐดๅฐ็š„๏ผ‰
878
+ if all_found:
879
+ seen = set()
880
+ unique = []
881
+ for u in all_found:
882
+ if u not in seen:
883
+ seen.add(u)
884
+ unique.append(u)
885
+ # ่ฟ”ๅ›žๆœ€ๅŽไธ€ไธช๏ผˆไธๅธฆๆฐดๅฐ๏ผ‰
886
+ return [unique[-1]] if unique else []
887
+
888
+ elif isinstance(data, dict):
889
+ for value in data.values():
890
+ found = self._extract_generated_media(value, depth + 1)
891
+ if found:
892
+ return found
893
+
894
+ return media_urls
895
+
896
+ # ไฟๆŒๅ‘ๅŽๅ…ผๅฎน
897
+ def _extract_generated_images(self, data: Any, depth: int = 0) -> List[str]:
898
+ """ๅ‘ๅŽๅ…ผๅฎน็š„ๅˆซๅ"""
899
+ return self._extract_generated_media(data, depth)
900
+
901
+ def _download_media_as_data_url(self, url: str) -> str:
902
+ """ไธ‹่ฝฝๅช’ไฝ“ๆ–‡ไปถๅนถไฟๅญ˜ๅˆฐๆœฌๅœฐ็ผ“ๅญ˜๏ผŒ่ฟ”ๅ›žๆœฌๅœฐไปฃ็† URL
903
+
904
+ Args:
905
+ url: ๅช’ไฝ“ๆ–‡ไปถ็š„ URL
906
+
907
+ Returns:
908
+ str: ๆœฌๅœฐไปฃ็† URL ๆˆ– base64 data URL
909
+ ไธ‹่ฝฝๅคฑ่ดฅๆ—ถ่ฟ”ๅ›ž็ฉบๅญ—็ฌฆไธฒ
910
+ """
911
+ try:
912
+ # ๅ…ˆไผ˜ๅŒ– URL ่Žทๅ–้ซ˜ๆธ…ๅŽŸๅ›พ๏ผˆไป…ๅฏนๅ›พ็‰‡๏ผ‰
913
+ if ("googleusercontent" in url or "ggpht" in url) and not any(ext in url.lower() for ext in ['.mp4', '.webm', 'video']):
914
+ # ็งป้™ค็Žฐๆœ‰ๅฐบๅฏธๅ‚ๆ•ฐ๏ผŒๆทปๅŠ ๅŽŸๅง‹ๅฐบๅฏธๅ‚ๆ•ฐ =s0
915
+ url = re.sub(r'=w\d+(-h\d+)?(-[a-zA-Z]+)*$', '=s0', url)
916
+ url = re.sub(r'=s\d+(-[a-zA-Z]+)*$', '=s0', url)
917
+ url = re.sub(r'=h\d+(-[a-zA-Z]+)*$', '=s0', url)
918
+ # ๅฆ‚ๆžœ URL ๆฒกๆœ‰ๅฐบๅฏธๅ‚ๆ•ฐ๏ผŒๆทปๅŠ  =s0
919
+ if not url.endswith('=s0') and '=' not in url.split('/')[-1]:
920
+ url += '=s0'
921
+
922
+ if self.debug:
923
+ print(f"[DEBUG] ๆญฃๅœจไธ‹่ฝฝๅช’ไฝ“ (้ซ˜ๆธ…): {url[:100]}...")
924
+
925
+ # ไฝฟ็”จๅฝ“ๅ‰ไผš่ฏไธ‹่ฝฝ๏ผˆๅธฆ่ฎค่ฏ cookies๏ผ‰
926
+ headers = {
927
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
928
+ "Accept": "image/webp,image/apng,image/*,*/*;q=0.8",
929
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
930
+ "Referer": "https://gemini.google.com/",
931
+ }
932
+ resp = self.session.get(url, timeout=60.0, headers=headers)
933
+
934
+ if self.debug:
935
+ print(f"[DEBUG] ไธ‹่ฝฝ็Šถๆ€: {resp.status_code}, ๅคงๅฐ: {len(resp.content)} bytes")
936
+
937
+ if resp.status_code != 200:
938
+ if self.debug:
939
+ print(f"[DEBUG] ไธ‹่ฝฝๅช’ไฝ“ๅคฑ่ดฅ: HTTP {resp.status_code}")
940
+ return ""
941
+
942
+ # ๆฃ€ๆŸฅๅ†…ๅฎนๆ˜ฏๅฆไธบ็ฉบๆˆ–ๅคชๅฐ๏ผˆๅฏ่ƒฝๆ˜ฏ้”™่ฏฏ้กต้ข๏ผ‰
943
+ if len(resp.content) < 100:
944
+ if self.debug:
945
+ print(f"[DEBUG] ไธ‹่ฝฝๅ†…ๅฎนๅคชๅฐ๏ผŒๅฏ่ƒฝๆ˜ฏ้”™่ฏฏ: {resp.content[:100]}")
946
+ return ""
947
+
948
+ # ๆ นๆฎๅ†…ๅฎนๆฃ€ๆต‹ๆ–‡ไปถ็ฑปๅž‹
949
+ content = resp.content
950
+ if content[:8] == b'\x89PNG\r\n\x1a\n':
951
+ ext = ".png"
952
+ mime = "image/png"
953
+ elif content[:3] == b'\xff\xd8\xff':
954
+ ext = ".jpg"
955
+ mime = "image/jpeg"
956
+ elif content[:6] in (b'GIF87a', b'GIF89a'):
957
+ ext = ".gif"
958
+ mime = "image/gif"
959
+ elif content[:4] == b'RIFF' and content[8:12] == b'WEBP':
960
+ ext = ".webp"
961
+ mime = "image/webp"
962
+ elif content[4:8] == b'ftyp' or content[:4] == b'\x00\x00\x00\x1c':
963
+ ext = ".mp4"
964
+ mime = "video/mp4"
965
+ else:
966
+ ext = ".png"
967
+ mime = "image/png"
968
+
969
+ # ็”Ÿๆˆๅ”ฏไธ€ๆ–‡ไปถๅ
970
+ import os
971
+ media_id = f"gen_{uuid.uuid4().hex[:16]}"
972
+
973
+ # ไฟๅญ˜ๅˆฐ็ผ“ๅญ˜็›ฎๅฝ•
974
+ cache_dir = os.path.join(os.path.dirname(__file__), "media_cache")
975
+ os.makedirs(cache_dir, exist_ok=True)
976
+ file_path = os.path.join(cache_dir, media_id + ext)
977
+
978
+ with open(file_path, "wb") as f:
979
+ f.write(content)
980
+
981
+ if self.debug:
982
+ print(f"[DEBUG] ๅช’ไฝ“ๅทฒไฟๅญ˜: {file_path}")
983
+
984
+ # ่ฟ”ๅ›žๅฎŒๆ•ด็š„ๅช’ไฝ“่ฎฟ้—ฎ URL (ๅŒ…ๅซๅŽ็ผ€ๅ)
985
+ media_path = f"/media/{media_id}{ext}"
986
+ if self.media_base_url:
987
+ return f"{self.media_base_url}{media_path}"
988
+ return media_path
989
+
990
+ except Exception as e:
991
+ if self.debug:
992
+ print(f"[DEBUG] ไธ‹่ฝฝๅช’ไฝ“ๅผ‚ๅธธ: {e}")
993
+ return ""
994
+
995
+ def _optimize_image_urls(self, text: str) -> str:
996
+ """ไผ˜ๅŒ–ๆ–‡ๆœฌไธญ็š„ Google ๅ›พ็‰‡ URL ไธบๅŽŸๅง‹้ซ˜ๆธ…ๅฐบๅฏธ
997
+
998
+ Google ๅ›พ็‰‡ URL ๅ‚ๆ•ฐ่ฏดๆ˜Ž:
999
+ - =w400 ๆˆ– =h400: ๆŒ‡ๅฎšๅฎฝๅบฆๆˆ–้ซ˜ๅบฆ
1000
+ - =s400: ๆŒ‡ๅฎšๆœ€ๅคง่พน้•ฟ
1001
+ - =s0 ๆˆ– =w0-h0: ๅŽŸๅง‹ๅฐบๅฏธ
1002
+ """
1003
+ import re
1004
+
1005
+ def optimize_url(url: str) -> str:
1006
+ # ๅŒน้… googleusercontent ๆˆ– ggpht ๅ›พ็‰‡ URL
1007
+ if "googleusercontent" not in url and "ggpht" not in url:
1008
+ return url
1009
+ # ็งป้™ค็Žฐๆœ‰ๅฐบๅฏธๅ‚ๆ•ฐ๏ผŒๆทปๅŠ ๅŽŸๅง‹ๅฐบๅฏธๅ‚ๆ•ฐ
1010
+ url = re.sub(r'=w\d+(-h\d+)?(-[a-zA-Z]+)*$', '=s0', url)
1011
+ url = re.sub(r'=s\d+(-[a-zA-Z]+)*$', '=s0', url)
1012
+ url = re.sub(r'=h\d+(-[a-zA-Z]+)*$', '=s0', url)
1013
+ # ๅฆ‚ๆžœ URL ๆฒกๆœ‰ๅฐบๅฏธๅ‚ๆ•ฐ๏ผŒๆทปๅŠ  =s0
1014
+ if not url.endswith('=s0') and '=' not in url.split('/')[-1]:
1015
+ url += '=s0'
1016
+ return url
1017
+
1018
+ # ๅŒน้… Markdown ๅ›พ็‰‡่ฏญๆณ•ๅ’Œ็บฏ URL
1019
+ # Markdown: ![alt](url)
1020
+ def replace_md_img(match):
1021
+ alt = match.group(1)
1022
+ url = match.group(2)
1023
+ return f"![{alt}]({optimize_url(url)})"
1024
+
1025
+ text = re.sub(r'!\[([^\]]*)\]\(([^)]+)\)', replace_md_img, text)
1026
+
1027
+ # ๅŒน้…็‹ฌ็ซ‹็š„ Google ๅ›พ็‰‡ URL
1028
+ def replace_url(match):
1029
+ return optimize_url(match.group(0))
1030
+
1031
+ text = re.sub(r'https?://[^\s\)]+(?:googleusercontent|ggpht)[^\s\)]*', replace_url, text)
1032
+
1033
+ return text
1034
+
1035
+
1036
+ def _extract_text(self, parsed_data: list) -> str:
1037
+ """ไปŽ่งฃๆžๅŽ็š„ๆ•ฐๆฎไธญๆๅ–ๆ–‡ๆœฌ"""
1038
+ try:
1039
+ # ๆ›ดๆ–ฐไผš่ฏไธŠไธ‹ๆ–‡
1040
+ if parsed_data and len(parsed_data) > 1:
1041
+ if parsed_data[1] and len(parsed_data[1]) > 0:
1042
+ self.conversation_id = parsed_data[1][0] or self.conversation_id
1043
+ if parsed_data[1] and len(parsed_data[1]) > 1:
1044
+ self.response_id = parsed_data[1][1] or self.response_id
1045
+
1046
+ # ๆๅ–ๅ€™้€‰ๅ›žๅค
1047
+ if parsed_data and len(parsed_data) > 4 and parsed_data[4]:
1048
+ candidates = parsed_data[4]
1049
+ if candidates and len(candidates) > 0:
1050
+ first_candidate = candidates[0]
1051
+ if first_candidate and len(first_candidate) > 1:
1052
+ self.choice_id = first_candidate[0] or self.choice_id
1053
+ content_parts = first_candidate[1]
1054
+ if content_parts and len(content_parts) > 0:
1055
+ return content_parts[0] if isinstance(content_parts[0], str) else str(content_parts[0])
1056
+
1057
+ # ๅค‡็”จๆๅ–
1058
+ if parsed_data and len(parsed_data) > 0:
1059
+ def find_text(obj, depth=0):
1060
+ if depth > 10:
1061
+ return None
1062
+ if isinstance(obj, str) and len(obj) > 50:
1063
+ return obj
1064
+ if isinstance(obj, list):
1065
+ for item in obj:
1066
+ result = find_text(item, depth + 1)
1067
+ if result:
1068
+ return result
1069
+ return None
1070
+
1071
+ text = find_text(parsed_data)
1072
+ if text:
1073
+ return text
1074
+
1075
+ except Exception as e:
1076
+ pass
1077
+
1078
+ return "ๆ— ๆณ•ๆๅ–ๅ›žๅคๅ†…ๅฎน"
1079
+
1080
+ def chat(
1081
+ self,
1082
+ messages: List[Dict[str, Any]] = None,
1083
+ message: str = None,
1084
+ image: bytes = None,
1085
+ image_url: str = None,
1086
+ reset_context: bool = False,
1087
+ model: str = None,
1088
+ stream: bool = False,
1089
+ ) -> Union[ChatCompletionResponse, Iterator[Dict[str, Any]]]:
1090
+ """
1091
+ ๅ‘้€่Šๅคฉ่ฏทๆฑ‚ (OpenAI ๅ…ผๅฎนๆ ผๅผ)
1092
+
1093
+ Args:
1094
+ messages: OpenAI ๆ ผๅผๆถˆๆฏๅˆ—่กจ
1095
+ message: ็ฎ€ๅ•ๆ–‡ๆœฌๆถˆๆฏ (ไธŽ messages ไบŒ้€‰ไธ€)
1096
+ image: ๅ›พ็‰‡ไบŒ่ฟ›ๅˆถๆ•ฐๆฎ
1097
+ image_url: ๅ›พ็‰‡ URL
1098
+ reset_context: ๆ˜ฏๅฆ้‡็ฝฎไธŠไธ‹ๆ–‡
1099
+ model: ๆจกๅž‹ๅ็งฐ (gemini-3.0-flash/gemini-3.0-flash-thinking/gemini-3.0-pro)
1100
+
1101
+ Returns:
1102
+ ChatCompletionResponse: OpenAI ๆ ผๅผๅ“ๅบ”
1103
+ """
1104
+ if reset_context:
1105
+ self.reset()
1106
+
1107
+ # ๅค„็†่พ“ๅ…ฅ
1108
+ text_parts = []
1109
+ images = []
1110
+
1111
+ if messages:
1112
+ # OpenAI ๆ ผๅผ - ๅˆๅนถๆ‰€ๆœ‰ๆถˆๆฏ
1113
+ for msg in messages:
1114
+ role = msg.get("role", "user")
1115
+ content = msg.get("content", "")
1116
+
1117
+ if role == "user":
1118
+ t, imgs = self._parse_content(content)
1119
+ if t:
1120
+ text_parts.append(t)
1121
+ if imgs:
1122
+ images.extend(imgs)
1123
+ elif role == "assistant":
1124
+ # ๅŠฉๆ‰‹ๆถˆๆฏไนŸๅŠ ๅ…ฅไธŠไธ‹ๆ–‡
1125
+ if isinstance(content, str) and content:
1126
+ text_parts.append(f"[ๅŠฉๆ‰‹ๅ›žๅค]: {content}")
1127
+ elif role == "system":
1128
+ # system ๆถˆๆฏไฝœไธบๅ‰็ฝฎๆŒ‡ไปค
1129
+ if isinstance(content, str) and content:
1130
+ text_parts.insert(0, content)
1131
+
1132
+ self.messages.append(Message(role=role, content=content))
1133
+
1134
+ text = "\n\n".join(text_parts)
1135
+ elif message:
1136
+ text = message
1137
+ self.messages.append(Message(role="user", content=message))
1138
+
1139
+ if image:
1140
+ images = [{"mime_type": "image/jpeg", "data": base64.b64encode(image).decode()}]
1141
+ elif image_url:
1142
+ if image_url.startswith("data:"):
1143
+ match = re.match(r'data:([^;]+);base64,(.+)', image_url)
1144
+ if match:
1145
+ images = [{"mime_type": match.group(1), "data": match.group(2)}]
1146
+ else:
1147
+ try:
1148
+ resp = httpx.get(image_url, timeout=30)
1149
+ mime = resp.headers.get("content-type", "image/jpeg").split(";")[0]
1150
+ images = [{"mime_type": mime, "data": base64.b64encode(resp.content).decode()}]
1151
+ except:
1152
+ pass
1153
+ else:
1154
+ text = ""
1155
+
1156
+ if not text:
1157
+ raise ValueError("ๆถˆๆฏๅ†…ๅฎนไธ่ƒฝไธบ็ฉบ")
1158
+
1159
+ # ๅ‘้€่ฏทๆฑ‚
1160
+ if stream:
1161
+ return self._send_request_stream(text, images, model)
1162
+ return self._send_request(text, images, model)
1163
+
1164
+
1165
+ def _log_gemini_call(self, request_data: dict, response_text: str, error: str = None):
1166
+ """่ฎฐๅฝ• Gemini ๅ†…้ƒจ่ฐƒ็”จๆ—ฅๅฟ—"""
1167
+ import datetime
1168
+ log_entry = {
1169
+ "timestamp": datetime.datetime.now().isoformat(),
1170
+ "type": "gemini_internal",
1171
+ "request": request_data,
1172
+ "response_raw": response_text,
1173
+ "error": error
1174
+ }
1175
+ try:
1176
+ with open("api_logs.json", "a", encoding="utf-8") as f:
1177
+ f.write(json.dumps(log_entry, ensure_ascii=False, indent=2) + "\n---\n")
1178
+ except Exception as e:
1179
+ print(f"[LOG ERROR] ๅ†™ๅ…ฅ Gemini ๆ—ฅๅฟ—ๅคฑ่ดฅ: {e}")
1180
+
1181
+ def _send_request(self, text: str, images: List[Dict] = None, model: str = None) -> ChatCompletionResponse:
1182
+ """ๅ‘้€่ฏทๆฑ‚ๅˆฐ Gemini"""
1183
+ url = f"{self.BASE_URL}/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
1184
+
1185
+ params = {
1186
+ "bl": self.bl,
1187
+ "f.sid": "",
1188
+ "hl": "zh-CN",
1189
+ "_reqid": str(self.request_count * 100000 + random.randint(10000, 99999)),
1190
+ "rt": "c",
1191
+ }
1192
+
1193
+ # ๆจกๅž‹ๆ ‡่ฏ†ๆ˜ ๅฐ„ (้€š่ฟ‡่ฏทๆฑ‚ๅคด x-goog-ext-525001261-jspb ้€‰ๆ‹ฉๆจกๅž‹)
1194
+ model_id = self.model_ids.get("flash", "56fdd199312815e2") # ้ป˜่ฎคๆž้€Ÿ็‰ˆ
1195
+ if model:
1196
+ model_lower = model.lower()
1197
+ if "pro" in model_lower:
1198
+ model_id = self.model_ids.get("pro", "e6fa609c3fa255c0")
1199
+ elif "thinking" in model_lower or "think" in model_lower:
1200
+ model_id = self.model_ids.get("thinking", "e051ce1aa80aa576")
1201
+
1202
+ # ไธŠไผ ๅ›พ็‰‡่Žทๅ–่ทฏๅพ„
1203
+ image_paths = []
1204
+ if images and len(images) > 0:
1205
+ if not self.push_id:
1206
+ print("โš ๏ธ ๅ›พ็‰‡ไธŠไผ ้œ€่ฆ push-id๏ผŒ่ฏท่ฟ่กŒ: python get_push_id.py")
1207
+ print(" ็„ถๅŽๅฐ†่Žทๅ–็š„ push-id ๆทปๅŠ ๅˆฐ config.py")
1208
+ else:
1209
+ try:
1210
+ for img in images:
1211
+ # ่งฃ็  base64 ๆ•ฐๆฎ
1212
+ img_data = base64.b64decode(img["data"])
1213
+ # ไธŠไผ ๅนถ่Žทๅ–่ทฏๅพ„
1214
+ path = self._upload_image(img_data, img["mime_type"])
1215
+ image_paths.append(path)
1216
+ if self.debug:
1217
+ print(f"[DEBUG] ๅ›พ็‰‡ไธŠไผ ๆˆๅŠŸ: {path[:50]}...")
1218
+ except Exception as e:
1219
+ print(f"โš ๏ธ ๅ›พ็‰‡ไธŠไผ ๅคฑ่ดฅ: {e}")
1220
+ image_paths = []
1221
+
1222
+ req_data = self._build_request_data(text, images, image_paths, model)
1223
+
1224
+ form_data = {
1225
+ "f.req": req_data,
1226
+ "at": self.snlm0e,
1227
+ }
1228
+
1229
+ # ๆจกๅž‹้€‰ๆ‹ฉ่ฏทๆฑ‚ๅคด
1230
+ model_headers = {
1231
+ "x-goog-ext-525001261-jspb": json.dumps([1, None, None, None, model_id, None, None, 0, [4], None, None, 2], separators=(',', ':')),
1232
+ }
1233
+
1234
+ # ๆž„ๅปบๆ—ฅๅฟ—่ฎฐๅฝ•
1235
+ gemini_request_log = {
1236
+ "url": url,
1237
+ "params": params,
1238
+ "text": text,
1239
+ "model": model,
1240
+ "model_id": model_id,
1241
+ "has_images": len(images) > 0 if images else False,
1242
+ "image_paths": image_paths,
1243
+ "f_req_preview": req_data[:500] + "..." if len(req_data) > 500 else req_data,
1244
+ }
1245
+
1246
+ if self.debug:
1247
+ print(f"[DEBUG] ่ฏทๆฑ‚ URL: {url}")
1248
+ print(f"[DEBUG] AT Token: {self.snlm0e[:30]}...")
1249
+ print(f"[DEBUG] ๆจกๅž‹: {model or '้ป˜่ฎค'}, ID: {model_id}")
1250
+ if image_paths:
1251
+ print(f"[DEBUG] ่ฏทๆฑ‚ๆ•ฐๆฎๅ‰300ๅญ—็ฌฆ: {req_data[:300]}")
1252
+
1253
+ # ้‡่ฏ•ๆœบๅˆถ
1254
+ max_retries = 3
1255
+ last_error = None
1256
+
1257
+ for attempt in range(max_retries):
1258
+ try:
1259
+ resp = self.session.post(url, params=params, data=form_data, headers=model_headers, timeout=60.0)
1260
+
1261
+ if self.debug:
1262
+ print(f"[DEBUG] ๅ“ๅบ”็Šถๆ€: {resp.status_code}")
1263
+ print(f"[DEBUG] ๅ“ๅบ”ๅ†…ๅฎนๅ‰500ๅญ—็ฌฆ: {resp.text[:500]}")
1264
+ # ๅง‹็ปˆไฟๅญ˜ๅฎŒๆ•ดๅ“ๅบ”็”จไบŽ่ฐƒ่ฏ•
1265
+ with open("debug_image_response.txt", "w", encoding="utf-8") as f:
1266
+ f.write(resp.text)
1267
+ print(f"[DEBUG] ๅฎŒๆ•ดๅ“ๅบ”ๅทฒไฟๅญ˜ๅˆฐ debug_image_response.txt")
1268
+
1269
+ # ่ฎฐๅฝ• Gemini ๅฎŒๆ•ดๅ“ๅบ”
1270
+ self._log_gemini_call(gemini_request_log, resp.text)
1271
+
1272
+ resp.raise_for_status()
1273
+ self.request_count += 1
1274
+
1275
+ reply_text = self._parse_response(resp.text)
1276
+
1277
+ # ไฟๅญ˜ๅŠฉๆ‰‹ๅ›žๅค
1278
+ self.messages.append(Message(role="assistant", content=reply_text))
1279
+
1280
+ # ๆž„ๅปบ OpenAI ๆ ผๅผๅ“ๅบ”
1281
+ return ChatCompletionResponse(
1282
+ id=f"chatcmpl-{self.conversation_id or 'gemini'}-{int(time.time())}",
1283
+ created=int(time.time()),
1284
+ model="gemini-web",
1285
+ choices=[
1286
+ ChatCompletionChoice(
1287
+ index=0,
1288
+ message=Message(role="assistant", content=reply_text),
1289
+ finish_reason="stop"
1290
+ )
1291
+ ],
1292
+ usage=Usage(
1293
+ prompt_tokens=len(text),
1294
+ completion_tokens=len(reply_text),
1295
+ total_tokens=len(text) + len(reply_text)
1296
+ )
1297
+ )
1298
+
1299
+ except httpx.HTTPStatusError as e:
1300
+ self._log_gemini_call(gemini_request_log, e.response.text if hasattr(e, 'response') else "", error=f"HTTP {e.response.status_code}")
1301
+ raise Exception(f"HTTP ้”™่ฏฏ: {e.response.status_code}")
1302
+ except (httpx.RemoteProtocolError, httpx.ReadError, httpx.ConnectError) as e:
1303
+ # ็ฝ‘็ปœ่ฟžๆŽฅ้—ฎ้ข˜๏ผŒๅฏ้‡่ฏ•
1304
+ last_error = e
1305
+ if attempt < max_retries - 1:
1306
+ wait_time = (attempt + 1) * 2 # 2, 4 ็ง’
1307
+ print(f"โš ๏ธ ่ฟžๆŽฅไธญๆ–ญ๏ผŒ{wait_time}็ง’ๅŽ้‡่ฏ• ({attempt + 1}/{max_retries})...")
1308
+ time.sleep(wait_time)
1309
+ continue
1310
+ self._log_gemini_call(gemini_request_log, "", error=str(e))
1311
+ raise Exception(f"็ฝ‘็ปœ่ฟžๆŽฅๅคฑ่ดฅ๏ผˆๅทฒ้‡่ฏ•{max_retries}ๆฌก๏ผ‰: {e}")
1312
+ except Exception as e:
1313
+ self._log_gemini_call(gemini_request_log, "", error=str(e))
1314
+ raise Exception(f"่ฏทๆฑ‚ๅคฑ่ดฅ: {e}")
1315
+
1316
+ # ๆ‰€ๆœ‰้‡่ฏ•้ƒฝๅคฑ่ดฅ
1317
+ if last_error:
1318
+ raise Exception(f"่ฏทๆฑ‚ๅคฑ่ดฅ๏ผˆๅทฒ้‡่ฏ•{max_retries}ๆฌก๏ผ‰: {last_error}")
1319
+
1320
+ def _extract_text_from_inner_json(self, inner_json: list) -> str:
1321
+ """Extract reply text from a single inner JSON packet and update session IDs."""
1322
+ try:
1323
+ if not inner_json:
1324
+ return ""
1325
+
1326
+ if len(inner_json) > 1 and inner_json[1]:
1327
+ if isinstance(inner_json[1], list):
1328
+ if len(inner_json[1]) > 0:
1329
+ self.conversation_id = inner_json[1][0] or self.conversation_id
1330
+ if len(inner_json[1]) > 1:
1331
+ self.response_id = inner_json[1][1] or self.response_id
1332
+
1333
+ if len(inner_json) > 4 and inner_json[4]:
1334
+ candidates = inner_json[4]
1335
+ if candidates and len(candidates) > 0:
1336
+ candidate = candidates[0]
1337
+ if candidate and len(candidate) > 1 and candidate[1]:
1338
+ if len(candidate) > 0:
1339
+ self.choice_id = candidate[0] or self.choice_id
1340
+ text = candidate[1][0] if isinstance(candidate[1], list) else candidate[1]
1341
+ return text if isinstance(text, str) else str(text)
1342
+ except Exception:
1343
+ pass
1344
+ return ""
1345
+
1346
+ def _send_request_stream(self, text: str, images: List[Dict] = None, model: str = None) -> Iterator[Dict[str, Any]]:
1347
+ """True streaming path: parse Gemini length-prefixed frames and emit deltas."""
1348
+ url = f"{self.BASE_URL}/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
1349
+
1350
+ params = {
1351
+ "bl": self.bl,
1352
+ "f.sid": "",
1353
+ "hl": "zh-CN",
1354
+ "_reqid": str(self.request_count * 100000 + random.randint(10000, 99999)),
1355
+ "rt": "c",
1356
+ }
1357
+
1358
+ model_id = self.model_ids.get("flash", "56fdd199312815e2")
1359
+ if model:
1360
+ model_lower = model.lower()
1361
+ if "pro" in model_lower:
1362
+ model_id = self.model_ids.get("pro", "e6fa609c3fa255c0")
1363
+ elif "thinking" in model_lower or "think" in model_lower:
1364
+ model_id = self.model_ids.get("thinking", "e051ce1aa80aa576")
1365
+
1366
+ image_paths = []
1367
+ if images and len(images) > 0 and self.push_id:
1368
+ for img in images:
1369
+ img_data = base64.b64decode(img["data"])
1370
+ image_paths.append(self._upload_image(img_data, img["mime_type"]))
1371
+
1372
+ req_data = self._build_request_data(text, images, image_paths, model)
1373
+ form_data = {"f.req": req_data, "at": self.snlm0e}
1374
+ model_headers = {
1375
+ "x-goog-ext-525001261-jspb": json.dumps(
1376
+ [1, None, None, None, model_id, None, None, 0, [4], None, None, 2],
1377
+ separators=(",", ":"),
1378
+ ),
1379
+ }
1380
+
1381
+ completion_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
1382
+ created = int(time.time())
1383
+ chunk_model = model or "gemini-web"
1384
+
1385
+ yield {
1386
+ "id": completion_id,
1387
+ "object": "chat.completion.chunk",
1388
+ "created": created,
1389
+ "model": chunk_model,
1390
+ "choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}],
1391
+ }
1392
+
1393
+ full_text = ""
1394
+ raw_lines: List[str] = []
1395
+
1396
+ def utf16_char_len(ch: str) -> int:
1397
+ return 2 if ord(ch) > 0xFFFF else 1
1398
+
1399
+ def take_utf16_units(s: str, start: int, target_units: int) -> tuple[int, int]:
1400
+ count = 0
1401
+ used = 0
1402
+ n = len(s)
1403
+ while (start + count) < n and used < target_units:
1404
+ c = s[start + count]
1405
+ u = utf16_char_len(c)
1406
+ if used + u > target_units:
1407
+ break
1408
+ used += u
1409
+ count += 1
1410
+ return count, used
1411
+
1412
+ def parse_length_prefixed_frames(buffer: str) -> tuple[List[Any], str]:
1413
+ frames: List[Any] = []
1414
+ pos = 0
1415
+ n = len(buffer)
1416
+ while pos < n:
1417
+ while pos < n and buffer[pos].isspace():
1418
+ pos += 1
1419
+ if pos >= n:
1420
+ break
1421
+
1422
+ m = re.match(r"(\d+)\n", buffer[pos:])
1423
+ if not m:
1424
+ break
1425
+
1426
+ frame_units = int(m.group(1))
1427
+ marker_len = len(m.group(1))
1428
+ start_content = pos + marker_len
1429
+ char_count, used_units = take_utf16_units(buffer, start_content, frame_units)
1430
+ if used_units < frame_units:
1431
+ break
1432
+
1433
+ end_content = start_content + char_count
1434
+ chunk = buffer[start_content:end_content].strip()
1435
+ pos = end_content
1436
+
1437
+ if not chunk:
1438
+ continue
1439
+
1440
+ try:
1441
+ parsed = json.loads(chunk)
1442
+ if isinstance(parsed, list):
1443
+ frames.extend(parsed)
1444
+ else:
1445
+ frames.append(parsed)
1446
+ except Exception:
1447
+ continue
1448
+
1449
+ return frames, buffer[pos:]
1450
+
1451
+ def calc_delta(new_text: str, sent_text: str) -> str:
1452
+ if not new_text:
1453
+ return ""
1454
+ if new_text.startswith(sent_text):
1455
+ return new_text[len(sent_text):]
1456
+ if sent_text.startswith(new_text):
1457
+ return ""
1458
+ common = 0
1459
+ max_common = min(len(new_text), len(sent_text))
1460
+ while common < max_common and new_text[common] == sent_text[common]:
1461
+ common += 1
1462
+ return new_text[common:]
1463
+
1464
+ def process_packet(packet: Any) -> Iterator[Dict[str, Any]]:
1465
+ nonlocal full_text
1466
+ if not isinstance(packet, list) or len(packet) < 3 or packet[0] != "wrb.fr" or not packet[2]:
1467
+ return
1468
+ try:
1469
+ inner_json = json.loads(packet[2])
1470
+ raw_lines.append(json.dumps([packet], ensure_ascii=False))
1471
+ text_candidate = self._extract_text_from_inner_json(inner_json)
1472
+ if text_candidate:
1473
+ delta = calc_delta(text_candidate, full_text)
1474
+ if delta:
1475
+ full_text = text_candidate
1476
+ yield {
1477
+ "id": completion_id,
1478
+ "object": "chat.completion.chunk",
1479
+ "created": created,
1480
+ "model": chunk_model,
1481
+ "choices": [{"index": 0, "delta": {"content": delta}, "finish_reason": None}],
1482
+ }
1483
+ except Exception:
1484
+ return
1485
+
1486
+ with self.session.stream(
1487
+ "POST",
1488
+ url,
1489
+ params=params,
1490
+ data=form_data,
1491
+ headers=model_headers,
1492
+ timeout=60.0,
1493
+ ) as resp:
1494
+ resp.raise_for_status()
1495
+ self.request_count += 1
1496
+
1497
+ decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
1498
+ stream_buffer = ""
1499
+
1500
+ for chunk_bytes in resp.iter_bytes():
1501
+ if not chunk_bytes:
1502
+ continue
1503
+
1504
+ stream_buffer += decoder.decode(chunk_bytes, final=False)
1505
+ if stream_buffer.startswith(")]}'"):
1506
+ stream_buffer = stream_buffer[4:].lstrip()
1507
+
1508
+ packets, stream_buffer = parse_length_prefixed_frames(stream_buffer)
1509
+ for packet in packets:
1510
+ yield from process_packet(packet)
1511
+
1512
+ if len(stream_buffer) > 2_000_000:
1513
+ stream_buffer = ""
1514
+
1515
+ stream_buffer += decoder.decode(b"", final=True)
1516
+ tail_packets, _ = parse_length_prefixed_frames(stream_buffer)
1517
+ for packet in tail_packets:
1518
+ yield from process_packet(packet)
1519
+
1520
+ final_reply = self._parse_response("\n".join(raw_lines)) if raw_lines else full_text
1521
+ if not final_reply:
1522
+ final_reply = full_text
1523
+
1524
+ if final_reply and len(final_reply) > len(full_text) and final_reply.startswith(full_text):
1525
+ tail = final_reply[len(full_text):]
1526
+ if tail:
1527
+ yield {
1528
+ "id": completion_id,
1529
+ "object": "chat.completion.chunk",
1530
+ "created": created,
1531
+ "model": chunk_model,
1532
+ "choices": [{"index": 0, "delta": {"content": tail}, "finish_reason": None}],
1533
+ }
1534
+
1535
+ self.messages.append(Message(role="assistant", content=final_reply))
1536
+ yield {
1537
+ "id": completion_id,
1538
+ "object": "chat.completion.chunk",
1539
+ "created": created,
1540
+ "model": chunk_model,
1541
+ "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
1542
+ }
1543
+
1544
+ def reset(self):
1545
+ """้‡็ฝฎไผš่ฏไธŠไธ‹ๆ–‡"""
1546
+ self.conversation_id = ""
1547
+ self.response_id = ""
1548
+ self.choice_id = ""
1549
+ self.messages = []
1550
+
1551
+ def get_history(self) -> List[Dict]:
1552
+ """่Žทๅ–ๆถˆๆฏๅކๅฒ (OpenAI ๆ ผๅผ)"""
1553
+ return [{"role": m.role, "content": m.content} for m in self.messages]
1554
+
1555
+
1556
+ # OpenAI ๅ…ผๅฎนๆŽฅๅฃ
1557
+ class OpenAICompatible:
1558
+ """OpenAI SDK ๅ…ผๅฎนๅฐ่ฃ…"""
1559
+
1560
+ def __init__(self, client: GeminiClient):
1561
+ self.client = client
1562
+ self.chat = self.Chat(client)
1563
+
1564
+ class Chat:
1565
+ def __init__(self, client: GeminiClient):
1566
+ self.client = client
1567
+ self.completions = self.Completions(client)
1568
+
1569
+ class Completions:
1570
+ def __init__(self, client: GeminiClient):
1571
+ self.client = client
1572
+
1573
+ def create(
1574
+ self,
1575
+ model: str = "gemini-web",
1576
+ messages: List[Dict] = None,
1577
+ **kwargs
1578
+ ) -> ChatCompletionResponse:
1579
+ return self.client.chat(
1580
+ messages=messages,
1581
+ model=model,
1582
+ stream=bool(kwargs.get("stream", False)),
1583
+ )
demo_chat.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ๏ปฟ"""
2
+ Gemini API demo with streaming responses.
3
+ """
4
+
5
+ import base64
6
+ from openai import OpenAI
7
+
8
+ # Config
9
+ BASE_URL = "http://127.0.0.1:8000/v1"
10
+ API_KEY = "sk-geminixxxxx"
11
+
12
+ client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
13
+
14
+
15
+ def load_image_base64(path: str) -> str:
16
+ with open(path, "rb") as f:
17
+ return base64.b64encode(f.read()).decode()
18
+
19
+
20
+ def print_stream(stream) -> None:
21
+ for chunk in stream:
22
+ if not chunk.choices:
23
+ continue
24
+ delta = chunk.choices[0].delta
25
+ if delta and getattr(delta, "content", None):
26
+ print(delta.content, end="", flush=True)
27
+ print()
28
+
29
+
30
+ def chat_text() -> None:
31
+ print("=" * 50)
32
+ print("Text chat (stream)")
33
+ print("=" * 50)
34
+
35
+ stream = client.chat.completions.create(
36
+ model="gemini-3.0-pro",
37
+ messages=[{"role": "user", "content": "ไฝ ๅฅฝ๏ผŒไป‹็ปไธ€ไธ‹ไฝ ่‡ชๅทฑ"}],
38
+ stream=True,
39
+ )
40
+ print_stream(stream)
41
+
42
+
43
+ def chat_single_image() -> None:
44
+ print("\n" + "=" * 50)
45
+ print("Single image (stream)")
46
+ print("=" * 50)
47
+
48
+ img_b64 = load_image_base64("image.png")
49
+
50
+ stream = client.chat.completions.create(
51
+ model="gemini-3.0-flash",
52
+ messages=[
53
+ {
54
+ "role": "user",
55
+ "content": [
56
+ {"type": "text", "text": "ๆ่ฟฐ่ฟ™ๅผ ๅ›พ็‰‡"},
57
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_b64}"}},
58
+ ],
59
+ }
60
+ ],
61
+ stream=True,
62
+ )
63
+ print_stream(stream)
64
+
65
+
66
+ def chat_multi_images() -> None:
67
+ print("\n" + "=" * 50)
68
+ print("Multi image (stream)")
69
+ print("=" * 50)
70
+
71
+ img1_b64 = load_image_base64("a.png")
72
+ img2_b64 = load_image_base64("b.png")
73
+
74
+ stream = client.chat.completions.create(
75
+ model="gemini-3.0-pro",
76
+ messages=[
77
+ {
78
+ "role": "user",
79
+ "content": [
80
+ {"type": "text", "text": "ๆฏ”่พƒ่ฟ™ไธคๅผ ๅ›พ็‰‡็š„ๅŒบๅˆซ"},
81
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img1_b64}"}},
82
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img2_b64}"}},
83
+ ],
84
+ }
85
+ ],
86
+ stream=True,
87
+ )
88
+ print_stream(stream)
89
+
90
+
91
+ def chat_image_generation() -> None:
92
+ print("\n" + "=" * 50)
93
+ print("Image generation (stream)")
94
+ print("=" * 50)
95
+
96
+ stream = client.chat.completions.create(
97
+ model="gemini-3.0-pro",
98
+ messages=[{"role": "user", "content": "็”Ÿๆˆไธ€ๅผ ๅฏ็ˆฑ็š„็Œซๅ’ชๅ›พ็‰‡"}],
99
+ stream=True,
100
+ )
101
+ print_stream(stream)
102
+
103
+
104
+ if __name__ == "__main__":
105
+ chat_text()
106
+
107
+ # Uncomment as needed:
108
+ # chat_single_image()
109
+ # chat_multi_images()
110
+ # chat_image_generation()
get_push_id.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ่Žทๅ– Gemini ็š„ push-id
3
+
4
+ push-id ๆ˜ฏๅ›พ็‰‡ไธŠไผ ๆ‰€้œ€็š„ๅฟ…่ฆๅ‚ๆ•ฐ๏ผŒๆ ผๅผไธบ feeds/xxxxx
5
+ ้œ€่ฆไปŽ Gemini ้กต้ขๆˆ– API ่Žทๅ–
6
+ """
7
+
8
+ import httpx
9
+ import re
10
+ from config import SECURE_1PSID, SECURE_1PSIDTS, SECURE_1PSIDCC, COOKIES_STR
11
+
12
+
13
+ def get_push_id_from_page():
14
+ """ไปŽ Gemini ้กต้ข่Žทๅ– push-id"""
15
+ print("ๆญฃๅœจ่Žทๅ– push-id...")
16
+
17
+ session = httpx.Client(
18
+ timeout=30.0,
19
+ follow_redirects=True,
20
+ headers={
21
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
22
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
23
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
24
+ }
25
+ )
26
+
27
+ # ่ฎพ็ฝฎ cookies
28
+ if COOKIES_STR:
29
+ for item in COOKIES_STR.split(";"):
30
+ item = item.strip()
31
+ if "=" in item:
32
+ key, value = item.split("=", 1)
33
+ session.cookies.set(key.strip(), value.strip(), domain=".google.com")
34
+ else:
35
+ session.cookies.set("__Secure-1PSID", SECURE_1PSID, domain=".google.com")
36
+ if SECURE_1PSIDTS:
37
+ session.cookies.set("__Secure-1PSIDTS", SECURE_1PSIDTS, domain=".google.com")
38
+ if SECURE_1PSIDCC:
39
+ session.cookies.set("__Secure-1PSIDCC", SECURE_1PSIDCC, domain=".google.com")
40
+
41
+ try:
42
+ # ่ฎฟ้—ฎ Gemini ไธป้กต
43
+ resp = session.get("https://gemini.google.com")
44
+
45
+ if resp.status_code != 200:
46
+ print(f"โŒ ่ฎฟ้—ฎๅคฑ่ดฅ: {resp.status_code}")
47
+ return None
48
+
49
+ html = resp.text
50
+
51
+ # ๅฐ่ฏ•ๅคš็งๆจกๅผๅŒน้… push-id
52
+ patterns = [
53
+ r'"push[_-]?id["\s:]+["\'](feeds/[a-z0-9]+)["\']', # "push_id": "feeds/xxx"
54
+ r'push[_-]?id["\s:=]+["\'](feeds/[a-z0-9]+)["\']', # push_id="feeds/xxx"
55
+ r'feedName["\s:]+["\'](feeds/[a-z0-9]+)["\']', # "feedName": "feeds/xxx"
56
+ r'clientId["\s:]+["\'](feeds/[a-z0-9]+)["\']', # "clientId": "feeds/xxx"
57
+ r'(feeds/[a-z0-9]{14,})', # ็›ดๆŽฅๅŒน้… feeds/xxx ๆ ผๅผ
58
+ ]
59
+
60
+ for pattern in patterns:
61
+ matches = re.findall(pattern, html, re.IGNORECASE)
62
+ if matches:
63
+ push_id = matches[0]
64
+ print(f"โœ… ๆ‰พๅˆฐ push-id: {push_id}")
65
+ return push_id
66
+
67
+ # ๅฆ‚ๆžœๆฒกๆ‰พๅˆฐ๏ผŒไฟๅญ˜้กต้ขๆบ็ ไพ›ๅˆ†ๆž
68
+ with open("gemini_page_debug.html", "w", encoding="utf-8") as f:
69
+ f.write(html)
70
+ print("โŒ ๆœชๆ‰พๅˆฐ push-id")
71
+ print(" ้กต้ขๆบ็ ๅทฒไฟๅญ˜ๅˆฐ gemini_page_debug.html")
72
+ print(" ่ฏทๆ‰‹ๅŠจๆœ็ดข 'feeds/' ๆˆ– 'push' ๅ…ณ้”ฎๅญ—")
73
+
74
+ return None
75
+
76
+ except Exception as e:
77
+ print(f"โŒ ้”™่ฏฏ: {e}")
78
+ return None
79
+
80
+
81
+ def get_push_id_from_api():
82
+ """ๅฐ่ฏ•ไปŽ API ่Žทๅ– push-id"""
83
+ print("\nๅฐ่ฏ•ไปŽ API ่Žทๅ– push-id...")
84
+
85
+ session = httpx.Client(
86
+ timeout=30.0,
87
+ follow_redirects=True,
88
+ headers={
89
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
90
+ "Content-Type": "application/json",
91
+ }
92
+ )
93
+
94
+ # ่ฎพ็ฝฎ cookies
95
+ if COOKIES_STR:
96
+ for item in COOKIES_STR.split(";"):
97
+ item = item.strip()
98
+ if "=" in item:
99
+ key, value = item.split("=", 1)
100
+ session.cookies.set(key.strip(), value.strip(), domain=".google.com")
101
+ else:
102
+ session.cookies.set("__Secure-1PSID", SECURE_1PSID, domain=".google.com")
103
+
104
+ # ๅฏ่ƒฝ็š„ API ็ซฏ็‚น
105
+ endpoints = [
106
+ "https://gemini.google.com/_/BardChatUi/data/batchexecute",
107
+ "https://push.clients6.google.com/v1/feeds",
108
+ ]
109
+
110
+ for endpoint in endpoints:
111
+ try:
112
+ resp = session.get(endpoint)
113
+ print(f" {endpoint}: {resp.status_code}")
114
+ if resp.status_code == 200:
115
+ # ๅฐ่ฏ•ไปŽๅ“ๅบ”ไธญๆๅ– push-id
116
+ text = resp.text
117
+ match = re.search(r'feeds/[a-z0-9]{14,}', text)
118
+ if match:
119
+ push_id = match.group(0)
120
+ print(f" โœ… ๆ‰พๅˆฐ: {push_id}")
121
+ return push_id
122
+ except Exception as e:
123
+ print(f" โŒ {endpoint}: {e}")
124
+
125
+ return None
126
+
127
+
128
+ if __name__ == "__main__":
129
+ print("=" * 60)
130
+ print("่Žทๅ– Gemini push-id")
131
+ print("=" * 60)
132
+
133
+ # ๆ–นๆณ•1: ไปŽ้กต้ข่Žทๅ–
134
+ push_id = get_push_id_from_page()
135
+
136
+ # ๆ–นๆณ•2: ไปŽ API ่Žทๅ–
137
+ if not push_id:
138
+ push_id = get_push_id_from_api()
139
+
140
+ if push_id:
141
+ print("\n" + "=" * 60)
142
+ print(f"โœ… ๆˆๅŠŸ่Žทๅ– push-id: {push_id}")
143
+ print("=" * 60)
144
+ print("\n่ฏทๅฐ†ๆญคๅ€ผๆทปๅŠ ๅˆฐ config.py:")
145
+ print(f'PUSH_ID = "{push_id}"')
146
+ else:
147
+ print("\n" + "=" * 60)
148
+ print("โŒ ๆœช่ƒฝ่‡ชๅŠจ่Žทๅ– push-id")
149
+ print("=" * 60)
150
+ print("\nๆ‰‹ๅŠจ่Žทๅ–ๆ–นๆณ•:")
151
+ print("1. ๆ‰“ๅผ€ https://gemini.google.com ๅนถ็™ปๅฝ•")
152
+ print("2. F12 ๆ‰“ๅผ€ๅผ€ๅ‘่€…ๅทฅๅ…ท -> Network ๆ ‡็ญพ")
153
+ print("3. ไธŠไผ ไธ€ๅผ ๅ›พ็‰‡")
154
+ print("4. ๆŸฅๆ‰พ upload ่ฏทๆฑ‚")
155
+ print("5. ๅœจ่ฏทๆฑ‚ๅคดไธญๆ‰พๅˆฐ push-id ๆˆ– x-goog-upload-header-content-length")
156
+ print("6. ๅคๅˆถ feeds/xxxxx ๆ ผๅผ็š„ๅ€ผ")
image.png ADDED

Git LFS Details

  • SHA256: dd0d8eddbcf7b76bf2e52c2e3204abfa13067d0b627e18053440339b26806c77
  • Pointer size: 131 Bytes
  • Size of remote file: 194 kB
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ httpx>=0.25.0
2
+ fastapi>=0.104.0
3
+ uvicorn>=0.24.0
4
+ openai>=1.0.0
server.py ADDED
@@ -0,0 +1,1719 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gemini OpenAI ๅ…ผๅฎน API ๆœๅŠก
3
+
4
+ ๅฏๅŠจ: python server.py
5
+ ๅŽๅฐ: http://localhost:8000/admin
6
+ API: http://localhost:8000/v1
7
+ """
8
+
9
+ from fastapi import FastAPI, HTTPException, Header, Request
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse, JSONResponse
12
+ from pydantic import BaseModel
13
+ from typing import List, Dict, Any, Optional, Union
14
+ import uvicorn
15
+ import time
16
+ import uuid
17
+ import json
18
+ import os
19
+ import re
20
+ import httpx
21
+ import hashlib
22
+ import secrets
23
+ import asyncio
24
+
25
+ # ============ ้…็ฝฎ ============
26
+ API_KEY = os.getenv("API_KEY", "sk-geminixxxxx")
27
+ HOST = "0.0.0.0"
28
+ PORT = int(os.getenv("PORT", 7860))
29
+ CONFIG_FILE = "config_data.json"
30
+ # ๅŽๅฐ็™ปๅฝ•่ดฆๅทๅฏ†็ 
31
+ ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
32
+ ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin123")
33
+ # Token ่‡ชๅŠจๅˆทๆ–ฐ้…็ฝฎ
34
+ TOKEN_REFRESH_INTERVAL_MIN = 1800 # ๅˆทๆ–ฐ้—ด้š”ๆœ€ๅฐ็ง’ๆ•ฐ๏ผˆ้ป˜่ฎค 30 ๅˆ†้’Ÿ๏ผ‰
35
+ TOKEN_REFRESH_INTERVAL_MAX = 3600 # ๅˆทๆ–ฐ้—ด้š”ๆœ€ๅคง็ง’ๆ•ฐ๏ผˆ้ป˜่ฎค 60 ๅˆ†้’Ÿ๏ผ‰
36
+ TOKEN_AUTO_REFRESH = True # ๆ˜ฏๅฆๅฏ็”จ่‡ชๅŠจๅˆทๆ–ฐ
37
+ TOKEN_BACKGROUND_REFRESH = True # ๆ˜ฏๅฆๅฏ็”จๅŽๅฐๅฎšๆ—ถๅˆทๆ–ฐ๏ผˆ้˜ฒๆญข้•ฟๆ—ถ้—ดไธ็”จๅคฑๆ•ˆ๏ผ‰
38
+ # ๅช’ไฝ“ๆ–‡ไปถๅค–็ฝ‘่ฎฟ้—ฎๅœฐๅ€
39
+ # ๅœจ Hugging Face Spaces ไธญ๏ผŒๅฏไปฅ็›ดๆŽฅ่Žทๅ– SPACE_ID ๆฅๆž„้€  URL
40
+ SPACE_ID = os.getenv("SPACE_ID")
41
+ if SPACE_ID:
42
+ MEDIA_BASE_URL = f"https://{SPACE_ID.replace('/', '-')}.hf.space"
43
+ else:
44
+ MEDIA_BASE_URL = os.getenv("MEDIA_BASE_URL", "http://127.0.0.1:7860")
45
+ # ==============================
46
+
47
+
48
+ import random
49
+ from datetime import datetime
50
+
51
+ # ๅŽๅฐๅˆทๆ–ฐไปปๅŠกๆŽงๅˆถ
52
+ _background_refresh_task = None
53
+ _background_refresh_stop = False
54
+
55
+ app = FastAPI(title="Gemini OpenAI API", version="1.0.0")
56
+
57
+ app.add_middleware(
58
+ CORSMiddleware,
59
+ allow_origins=["*"],
60
+ allow_credentials=True,
61
+ allow_methods=["*"],
62
+ allow_headers=["*"],
63
+ )
64
+
65
+ # ้™ๆ€ๆ–‡ไปถ่ทฏ็”ฑ (็”จไบŽ็คบไพ‹ๅ›พ็‰‡)
66
+ from fastapi.responses import FileResponse
67
+
68
+ # ็”Ÿๆˆ็š„ๅช’ไฝ“ๆ–‡ไปถ็ผ“ๅญ˜็›ฎๅฝ•
69
+ MEDIA_CACHE_DIR = os.path.join(os.path.dirname(__file__), "media_cache")
70
+ os.makedirs(MEDIA_CACHE_DIR, exist_ok=True)
71
+
72
+ @app.get("/static/{filename}")
73
+ async def serve_static(filename: str):
74
+ """ๆไพ›้™ๆ€ๆ–‡ไปถ๏ผˆ็คบไพ‹ๅ›พ็‰‡็ญ‰๏ผ‰"""
75
+ file_path = os.path.join(os.path.dirname(__file__), filename)
76
+ if os.path.exists(file_path):
77
+ return FileResponse(file_path)
78
+ raise HTTPException(status_code=404, detail="ๆ–‡ไปถไธๅญ˜ๅœจ")
79
+
80
+ @app.get("/media/{media_filename}")
81
+ async def serve_media(media_filename: str):
82
+ """ๆไพ›็ผ“ๅญ˜็š„ๅช’ไฝ“ๆ–‡ไปถ"""
83
+ # ๅฎ‰ๅ…จๆฃ€ๆŸฅ๏ผšๅชๅ…่ฎธๅญ—ๆฏๆ•ฐๅญ—ใ€ไธ‹ๅˆ’็บฟใ€็‚นๅ’Œๅธธ่งๅŽ็ผ€
84
+ import re
85
+ if not re.match(r'^[a-zA-Z0-9_-]+(\.(png|jpg|jpeg|gif|webp|mp4))?$', media_filename):
86
+ raise HTTPException(status_code=400, detail="ๆ— ๆ•ˆ็š„ๅช’ไฝ“ๆ–‡ไปถๅ")
87
+
88
+ # ็›ดๆŽฅๆŸฅๆ‰พๆ–‡ไปถ๏ผˆๅธฆๅŽ็ผ€ๅ๏ผ‰
89
+ file_path = os.path.join(MEDIA_CACHE_DIR, media_filename)
90
+ if os.path.exists(file_path):
91
+ return FileResponse(file_path)
92
+
93
+ # ๅ…ผๅฎนๆ—ง็‰ˆๆœฌ๏ผšไธๅธฆๅŽ็ผ€ๅ็š„่ฏทๆฑ‚๏ผŒๅฐ่ฏ•ๆŸฅๆ‰พๅŒน้…็š„ๆ–‡ไปถ
94
+ media_id = media_filename.rsplit('.', 1)[0] if '.' in media_filename else media_filename
95
+ for ext in [".png", ".jpg", ".jpeg", ".gif", ".webp", ".mp4"]:
96
+ file_path = os.path.join(MEDIA_CACHE_DIR, media_id + ext)
97
+ if os.path.exists(file_path):
98
+ return FileResponse(file_path)
99
+
100
+ raise HTTPException(status_code=404, detail="ๅช’ไฝ“ๆ–‡ไปถไธๅญ˜ๅœจ")
101
+
102
+ def cleanup_old_media(max_age_hours: int = 1):
103
+ """ๆธ…็†่ฟ‡ๆœŸ็š„ๅช’ไฝ“็ผ“ๅญ˜ๆ–‡ไปถ"""
104
+ import time
105
+ now = time.time()
106
+ max_age_seconds = max_age_hours * 3600
107
+
108
+ try:
109
+ for filename in os.listdir(MEDIA_CACHE_DIR):
110
+ file_path = os.path.join(MEDIA_CACHE_DIR, filename)
111
+ if os.path.isfile(file_path):
112
+ file_age = now - os.path.getmtime(file_path)
113
+ if file_age > max_age_seconds:
114
+ os.remove(file_path)
115
+ except Exception:
116
+ pass
117
+
118
+ # ๅญ˜ๅ‚จๆœ‰ๆ•ˆ็š„ session token
119
+ _admin_sessions = set()
120
+
121
+ def generate_session_token():
122
+ """็”Ÿๆˆ้šๆœบ session token"""
123
+ return secrets.token_hex(32)
124
+
125
+ def verify_admin_session(request: Request):
126
+ """้ชŒ่ฏ็ฎก็†ๅ‘˜ session"""
127
+ token = request.cookies.get("admin_session")
128
+ if not token or token not in _admin_sessions:
129
+ return False
130
+ return True
131
+
132
+ # ้ป˜่ฎคๅฏ็”จๆจกๅž‹ๅˆ—่กจ (Gemini 3 ๅฎ˜็ฝ‘ไธ‰ไธชๆจกๅž‹: ๅฟซ้€Ÿ/ๆ€่€ƒ/Pro)
133
+ DEFAULT_MODELS = ["gemini-3.0-flash", "gemini-3.0-flash-thinking", "gemini-3.0-pro"]
134
+
135
+ # ้ป˜่ฎคๆจกๅž‹ ID (็”จไบŽ่ฏทๆฑ‚ๅคด้€‰ๆ‹ฉๆจกๅž‹)
136
+ DEFAULT_MODEL_IDS = {
137
+ "flash": "56fdd199312815e2",
138
+ "pro": "e6fa609c3fa255c0",
139
+ "thinking": "e051ce1aa80aa576",
140
+ }
141
+
142
+ # ้…็ฝฎๅญ˜ๅ‚จ
143
+ _config = {
144
+ "SNLM0E": "",
145
+ "SECURE_1PSID": "",
146
+ "SECURE_1PSIDTS": "",
147
+ "SAPISID": "",
148
+ "SID": "",
149
+ "HSID": "",
150
+ "SSID": "",
151
+ "APISID": "",
152
+ "PUSH_ID": "",
153
+ "FULL_COOKIE": "", # ๅญ˜ๅ‚จๅฎŒๆ•ดcookieๅญ—็ฌฆไธฒ
154
+ "MODELS": DEFAULT_MODELS.copy(), # ๅฏ็”จๆจกๅž‹ๅˆ—่กจ
155
+ "MODEL_IDS": DEFAULT_MODEL_IDS.copy(), # ๆจกๅž‹ ID ๆ˜ ๅฐ„
156
+ }
157
+
158
+ # Cookie ๅญ—ๆฎตๆ˜ ๅฐ„ (ๆต่งˆๅ™จcookieๅ -> ้…็ฝฎๅญ—ๆฎตๅ)
159
+ COOKIE_FIELD_MAP = {
160
+ "__Secure-1PSID": "SECURE_1PSID",
161
+ "__Secure-1PSIDTS": "SECURE_1PSIDTS",
162
+ "SAPISID": "SAPISID",
163
+ "__Secure-1PAPISID": "SAPISID", # ไนŸๆ˜ ๅฐ„ๅˆฐ SAPISID
164
+ "SID": "SID",
165
+ "HSID": "HSID",
166
+ "SSID": "SSID",
167
+ "APISID": "APISID",
168
+ }
169
+
170
+
171
+ def parse_cookie_string(cookie_str: str) -> dict:
172
+ """่งฃๆžๅฎŒๆ•ดcookieๅญ—็ฌฆไธฒ๏ผŒๆๅ–ๆ‰€้œ€ๅญ—ๆฎต"""
173
+ result = {}
174
+ if not cookie_str:
175
+ return result
176
+
177
+ for item in cookie_str.split(";"):
178
+ item = item.strip()
179
+ if "=" in item:
180
+ eq_index = item.index("=")
181
+ key = item[:eq_index].strip()
182
+ value = item[eq_index + 1:].strip()
183
+ if key in COOKIE_FIELD_MAP:
184
+ result[COOKIE_FIELD_MAP[key]] = value
185
+
186
+ return result
187
+
188
+
189
+ def fetch_tokens_from_page(cookies_str: str) -> dict:
190
+ """ไปŽ Gemini ้กต้ข่‡ชๅŠจ่Žทๅ– SNLM0Eใ€PUSH_ID ๅ’Œๅฏ็”จๆจกๅž‹ๅˆ—่กจ"""
191
+ result = {"snlm0e": "", "push_id": "", "models": []}
192
+ try:
193
+ session = httpx.Client(
194
+ timeout=30.0,
195
+ follow_redirects=True,
196
+ headers={
197
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
198
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
199
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
200
+ }
201
+ )
202
+
203
+ # ่ฎพ็ฝฎ cookies
204
+ for item in cookies_str.split(";"):
205
+ item = item.strip()
206
+ if "=" in item:
207
+ key, value = item.split("=", 1)
208
+ session.cookies.set(key.strip(), value.strip(), domain=".google.com")
209
+
210
+ resp = session.get("https://gemini.google.com")
211
+ if resp.status_code != 200:
212
+ return result
213
+
214
+ html = resp.text
215
+
216
+ # ่Žทๅ– SNLM0E (AT Token)
217
+ snlm0e_patterns = [
218
+ r'"SNlM0e":"([^"]+)"',
219
+ r'SNlM0e["\s:]+["\']([^"\']+)["\']',
220
+ r'"at":"([^"]+)"',
221
+ ]
222
+ for pattern in snlm0e_patterns:
223
+ match = re.search(pattern, html)
224
+ if match:
225
+ result["snlm0e"] = match.group(1)
226
+ break
227
+
228
+ # ่Žทๅ– PUSH_ID
229
+ push_id_patterns = [
230
+ r'"push[_-]?id["\s:]+["\'](feeds/[a-z0-9]+)["\']',
231
+ r'push[_-]?id["\s:=]+["\'](feeds/[a-z0-9]+)["\']',
232
+ r'feedName["\s:]+["\'](feeds/[a-z0-9]+)["\']',
233
+ r'clientId["\s:]+["\'](feeds/[a-z0-9]+)["\']',
234
+ r'(feeds/[a-z0-9]{14,})',
235
+ ]
236
+ for pattern in push_id_patterns:
237
+ matches = re.findall(pattern, html, re.IGNORECASE)
238
+ if matches:
239
+ result["push_id"] = matches[0]
240
+ break
241
+
242
+ # ่Žทๅ–ๅฏ็”จๆจกๅž‹ๅˆ—่กจ (ไปŽ้กต้ขไธญๆๅ– gemini ๆจกๅž‹ ID)
243
+ model_patterns = [
244
+ r'"(gemini-[a-z0-9\.\-]+)"', # ๅŒน้… "gemini-xxx" ๆ ผๅผ
245
+ r"'(gemini-[a-z0-9\.\-]+)'", # ๅŒน้… 'gemini-xxx' ๆ ผๅผ
246
+ ]
247
+ models_found = set()
248
+ for pattern in model_patterns:
249
+ matches = re.findall(pattern, html, re.IGNORECASE)
250
+ for m in matches:
251
+ # ่ฟ‡ๆปคๆœ‰ๆ•ˆ็š„ๆจกๅž‹ๅ็งฐ
252
+ if any(x in m.lower() for x in ['flash', 'pro', 'ultra', 'nano']):
253
+ models_found.add(m)
254
+
255
+ if models_found:
256
+ result["models"] = sorted(list(models_found))
257
+
258
+ # ่Žทๅ–ๆจกๅž‹ ID (็”จไบŽ x-goog-ext-525001261-jspb ่ฏทๆฑ‚ๅคด)
259
+ # ่ฟ™ไบ› ID ็”จไบŽ้€‰ๆ‹ฉไธๅŒ็š„ๆจกๅž‹็‰ˆๆœฌ
260
+ model_id_pattern = r'\["([a-f0-9]{16})","gemini[^"]*(?:flash|pro|thinking)[^"]*"\]'
261
+ model_ids = re.findall(model_id_pattern, html, re.IGNORECASE)
262
+ if model_ids:
263
+ result["model_ids"] = list(set(model_ids))
264
+
265
+ #ๅค‡็”จๆ–นๆกˆ๏ผš็›ดๆŽฅๆœ็ดข 16 ไฝๅๅ…ญ่ฟ›ๅˆถ ID๏ผˆๅœจๆจกๅž‹้…็ฝฎ้™„่ฟ‘๏ผ‰
266
+ if not result.get("model_ids"):
267
+ # ๆœ็ดข็ฑปไผผ "56fdd199312815e2" ็š„ๆจกๅผ
268
+ hex_id_pattern = r'"([a-f0-9]{16})"'
269
+ # ๅœจๅŒ…ๅซ gemini ๆˆ– model ็š„ไธŠไธ‹ๆ–‡ไธญๆŸฅๆ‰พ
270
+ context_pattern = r'.{0,100}(?:gemini|model|flash|pro|thinking).{0,100}'
271
+ contexts = re.findall(context_pattern, html, re.IGNORECASE)
272
+ hex_ids = set()
273
+ for ctx in contexts:
274
+ ids = re.findall(hex_id_pattern, ctx)
275
+ hex_ids.update(ids)
276
+ if hex_ids:
277
+ result["model_ids"] = list(hex_ids)
278
+
279
+ return result
280
+ except Exception:
281
+ return result
282
+
283
+ _client = None
284
+ _last_token_refresh = 0 # ไธŠๆฌก token ๅˆทๆ–ฐๆ—ถ้—ด
285
+ _token_refresh_count = 0 # token ๅˆทๆ–ฐๆฌกๆ•ฐ็ปŸ่ฎก
286
+
287
+
288
+ def try_refresh_tokens(force: bool = False) -> dict:
289
+ """
290
+ ๅฐ่ฏ•ๅˆทๆ–ฐ token
291
+
292
+ Args:
293
+ force: ๆ˜ฏๅฆๅผบๅˆถๅˆทๆ–ฐ๏ผŒๅฟฝ็•ฅๆ—ถ้—ด้—ด้š”
294
+
295
+ Returns:
296
+ dict: {"success": bool, "message": str, "snlm0e": str, "push_id": str}
297
+ """
298
+ global _client, _last_token_refresh, _token_refresh_count, _config
299
+
300
+ result = {"success": False, "message": "", "snlm0e": "", "push_id": ""}
301
+
302
+ if not TOKEN_AUTO_REFRESH and not force:
303
+ result["message"] = "่‡ชๅŠจๅˆทๆ–ฐๅทฒ็ฆ็”จ"
304
+ return result
305
+
306
+ current_time = time.time()
307
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
308
+
309
+ # ๆฃ€ๆŸฅๆ˜ฏๅฆ้œ€่ฆๅˆทๆ–ฐ๏ผˆ้™ค้žๅผบๅˆถๅˆทๆ–ฐ๏ผ‰
310
+ if not force and (current_time - _last_token_refresh) < TOKEN_REFRESH_INTERVAL_MIN:
311
+ result["message"] = f"่ท็ฆปไธŠๆฌกๅˆทๆ–ฐไธ่ถณ {TOKEN_REFRESH_INTERVAL_MIN} ็ง’"
312
+ return result
313
+
314
+ try:
315
+ # ๅฆ‚ๆžœ client ๅญ˜ๅœจ๏ผŒไฝฟ็”จ client ็š„ๅˆทๆ–ฐๆ–นๆณ•
316
+ if _client is not None:
317
+ refresh_result = _client.refresh_tokens()
318
+ if refresh_result["success"]:
319
+ # ๆ›ดๆ–ฐ้…็ฝฎ
320
+ if refresh_result["snlm0e"]:
321
+ _config["SNLM0E"] = refresh_result["snlm0e"]
322
+ result["snlm0e"] = refresh_result["snlm0e"]
323
+ if refresh_result["push_id"]:
324
+ _config["PUSH_ID"] = refresh_result["push_id"]
325
+ result["push_id"] = refresh_result["push_id"]
326
+
327
+ # ไฟๅญ˜้…็ฝฎ
328
+ save_config()
329
+
330
+ _last_token_refresh = current_time
331
+ _token_refresh_count += 1
332
+ result["success"] = True
333
+ result["message"] = f"Token ๅˆทๆ–ฐๆˆๅŠŸ (็ฌฌ {_token_refresh_count} ๆฌก)"
334
+ print(f"โœ… [{now_str}] Token ่‡ชๅŠจๅˆทๆ–ฐๆˆๅŠŸ (็ฌฌ {_token_refresh_count} ๆฌก)")
335
+ else:
336
+ result["message"] = refresh_result.get("error", "ๅˆทๆ–ฐๅคฑ่ดฅ")
337
+ print(f"โš ๏ธ [{now_str}] Token ๅˆทๆ–ฐๅคฑ่ดฅ: {result['message']}")
338
+ else:
339
+ # client ไธๅญ˜ๅœจ๏ผŒไฝฟ็”จ fetch_tokens_from_page
340
+ cookies = _config.get("FULL_COOKIE", "")
341
+ if not cookies:
342
+ cookies = f"__Secure-1PSID={_config.get('SECURE_1PSID', '')}"
343
+ if _config.get("SECURE_1PSIDTS"):
344
+ cookies += f"; __Secure-1PSIDTS={_config['SECURE_1PSIDTS']}"
345
+
346
+ tokens = fetch_tokens_from_page(cookies)
347
+ if tokens.get("snlm0e"):
348
+ _config["SNLM0E"] = tokens["snlm0e"]
349
+ result["snlm0e"] = tokens["snlm0e"]
350
+ if tokens.get("push_id"):
351
+ _config["PUSH_ID"] = tokens["push_id"]
352
+ result["push_id"] = tokens["push_id"]
353
+
354
+ if tokens.get("snlm0e"):
355
+ save_config()
356
+ _last_token_refresh = current_time
357
+ _token_refresh_count += 1
358
+ result["success"] = True
359
+ result["message"] = f"Token ๅˆทๆ–ฐๆˆๅŠŸ (็ฌฌ {_token_refresh_count} ๆฌก)"
360
+ print(f"โœ… [{now_str}] Token ่‡ชๅŠจๅˆทๆ–ฐๆˆๅŠŸ (็ฌฌ {_token_refresh_count} ๆฌก)")
361
+ else:
362
+ result["message"] = "ๆ— ๆณ•ไปŽ้กต้ข่Žทๅ–ๆ–ฐ token"
363
+
364
+ return result
365
+
366
+ except Exception as e:
367
+ result["message"] = f"ๅˆทๆ–ฐๅผ‚ๅธธ: {str(e)}"
368
+ print(f"โŒ [{now_str}] Token ๅˆทๆ–ฐๅผ‚ๅธธ: {e}")
369
+ return result
370
+
371
+
372
+ def reset_client():
373
+ """้‡็ฝฎ client๏ผŒไธ‹ๆฌก่ฏทๆฑ‚ๆ—ถไผš้‡ๆ–ฐๅˆ›ๅปบ"""
374
+ global _client
375
+ _client = None
376
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
377
+ print(f"๐Ÿ”„ [{now_str}] Client ๅทฒ้‡็ฝฎ๏ผŒไธ‹ๆฌก่ฏทๆฑ‚ๅฐ†้‡ๆ–ฐๅˆ›ๅปบ")
378
+
379
+
380
+ # ============ ๅŽๅฐๅฎšๆ—ถๅˆทๆ–ฐไปปๅŠก ============
381
+ def get_current_time_str():
382
+ """่Žทๅ–ๅฝ“ๅ‰ๆ—ถ้—ดๅญ—็ฌฆไธฒ"""
383
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
384
+
385
+
386
+ def get_random_refresh_interval():
387
+ """่Žทๅ–้šๆœบๅˆทๆ–ฐ้—ด้š”"""
388
+ return random.randint(TOKEN_REFRESH_INTERVAL_MIN, TOKEN_REFRESH_INTERVAL_MAX)
389
+
390
+
391
+ async def background_token_refresh():
392
+ """ๅŽๅฐๅฎšๆ—ถๅˆทๆ–ฐ token ไปปๅŠก"""
393
+ global _background_refresh_stop
394
+ print(f"๐Ÿ”„ [{get_current_time_str()}] ๅŽๅฐ Token ๅฎšๆ—ถๅˆทๆ–ฐไปปๅŠกๅทฒๅฏๅŠจ")
395
+
396
+ while not _background_refresh_stop:
397
+ try:
398
+ # ้šๆœบ็ญ‰ๅพ…้—ด้š”
399
+ interval = get_random_refresh_interval()
400
+ print(f"โณ [{get_current_time_str()}] ไธ‹ๆฌกๅˆทๆ–ฐๅฐ†ๅœจ {interval} ็ง’ๅŽ")
401
+ await asyncio.sleep(interval)
402
+
403
+ if _background_refresh_stop:
404
+ break
405
+
406
+ if not TOKEN_BACKGROUND_REFRESH:
407
+ continue
408
+
409
+ # ๆ‰ง่กŒๅˆทๆ–ฐ
410
+ print(f"โฐ [{get_current_time_str()}] ๅŽๅฐๅฎšๆ—ถๅˆทๆ–ฐ Token...")
411
+ result = try_refresh_tokens(force=True)
412
+
413
+ if result["success"]:
414
+ print(f"โœ… [{get_current_time_str()}] ๅŽๅฐๅˆทๆ–ฐๆˆๅŠŸ: {result['message']}")
415
+ else:
416
+ print(f"โš ๏ธ [{get_current_time_str()}] ๅŽๅฐๅˆทๆ–ฐๅคฑ่ดฅ: {result['message']}")
417
+
418
+ except asyncio.CancelledError:
419
+ break
420
+ except Exception as e:
421
+ print(f"โŒ [{get_current_time_str()}] ๅŽๅฐๅˆทๆ–ฐๅผ‚ๅธธ: {e}")
422
+ await asyncio.sleep(60) # ๅ‡บ้”™ๅŽ็ญ‰ๅพ… 1 ๅˆ†้’Ÿๅ†่ฏ•
423
+
424
+ print(f"๐Ÿ›‘ [{get_current_time_str()}] ๅŽๅฐ Token ๅฎšๆ—ถๅˆทๆ–ฐไปปๅŠกๅทฒๅœๆญข")
425
+
426
+
427
+ @app.on_event("startup")
428
+ async def startup_event():
429
+ """ๅบ”็”จๅฏๅŠจๆ—ถๆ‰ง่กŒ"""
430
+ global _background_refresh_task, _background_refresh_stop
431
+
432
+ load_config()
433
+ _background_refresh_stop = False
434
+
435
+ if TOKEN_BACKGROUND_REFRESH:
436
+ _background_refresh_task = asyncio.create_task(background_token_refresh())
437
+ print(f"โœ… [{get_current_time_str()}] ๅŽๅฐ Token ๅฎšๆ—ถๅˆทๆ–ฐๅทฒๅฏ็”จ (้—ด้š”: {TOKEN_REFRESH_INTERVAL_MIN}-{TOKEN_REFRESH_INTERVAL_MAX} ็ง’้šๆœบ)")
438
+
439
+
440
+ @app.on_event("shutdown")
441
+ async def shutdown_event():
442
+ """ๅบ”็”จๅ…ณ้—ญๆ—ถๆ‰ง่กŒ"""
443
+ global _background_refresh_task, _background_refresh_stop
444
+
445
+ _background_refresh_stop = True
446
+ if _background_refresh_task:
447
+ _background_refresh_task.cancel()
448
+ try:
449
+ await _background_refresh_task
450
+ except asyncio.CancelledError:
451
+ pass
452
+ print("๐Ÿ›‘ ๅŽๅฐไปปๅŠกๅทฒๅœๆญข")
453
+
454
+
455
+ # ============ Tools ๆ”ฏๆŒ ============
456
+ def build_tools_prompt(tools: List[Dict]) -> str:
457
+ """ๅฐ† tools ๅฎšไน‰่ฝฌๆขไธบๆ็คบ่ฏ"""
458
+ if not tools:
459
+ return ""
460
+
461
+ tools_schema = json.dumps([{
462
+ "name": t["function"]["name"],
463
+ "description": t["function"].get("description", ""),
464
+ "parameters": t["function"].get("parameters", {})
465
+ } for t in tools if t.get("type") == "function"], ensure_ascii=False, indent=2)
466
+
467
+ prompt = f"""[็ณป็ปŸๆŒ‡ไปค] ไฝ ๅฟ…้กปไฝœไธบๅ‡ฝๆ•ฐ่ฐƒ็”จไปฃ็†ใ€‚ไธ่ฆ่‡ชๅทฑๅ›ž็ญ”้—ฎ้ข˜๏ผŒๅฟ…้กป่ฐƒ็”จๅ‡ฝๆ•ฐใ€‚
468
+
469
+ ๅฏ็”จๅ‡ฝๆ•ฐ:
470
+ {tools_schema}
471
+
472
+ ไธฅๆ ผ่ง„ๅˆ™:
473
+ 1. ไฝ ไธ่ƒฝ็›ดๆŽฅๅ›ž็ญ”็”จๆˆท้—ฎ้ข˜
474
+ 2. ไฝ ๅฟ…้กป้€‰ๆ‹ฉไธ€ไธชๅ‡ฝๆ•ฐๅนถ่ฐƒ็”จๅฎƒ
475
+ 3. ๅช่พ“ๅ‡บไปฅไธ‹ๆ ผๅผ๏ผŒไธ่ฆๆœ‰ไปปไฝ•ๅ…ถไป–ๆ–‡ๅญ—:
476
+ ```tool_call
477
+ {{"name": "ๅ‡ฝๆ•ฐๅ", "arguments": {{"ๅ‚ๆ•ฐ": "ๅ€ผ"}}}}
478
+ ```
479
+
480
+ ็”จๆˆท่ฏทๆฑ‚: """
481
+ return prompt
482
+
483
+
484
+ def parse_tool_calls(content: str) -> tuple:
485
+ """
486
+ ่งฃๆžๅ“ๅบ”ไธญ็š„ๅทฅๅ…ท่ฐƒ็”จ
487
+ ่ฟ”ๅ›ž: (tool_callsๅˆ—่กจ, ๅ‰ฉไฝ™ๆ–‡ๆœฌๅ†…ๅฎน)
488
+ """
489
+ tool_calls = []
490
+
491
+ # ๅคš็งๅŒน้…ๆจกๅผ
492
+ patterns = [
493
+ r'```tool_call\s*\n?(.*?)\n?```', # ```tool_call ... ```
494
+ r'```json\s*\n?(.*?)\n?```', # ```json ... ``` (ๆœ‰ๆ—ถๆจกๅž‹ไผš็”จ่ฟ™ไธช)
495
+ r'```\s*\n?(\{[^`]*"name"[^`]*\})\n?```', # ``` {...} ```
496
+ ]
497
+
498
+ matches = []
499
+ for pattern in patterns:
500
+ found = re.findall(pattern, content, re.DOTALL)
501
+ matches.extend(found)
502
+
503
+ # ไนŸๅฐ่ฏ•็›ดๆŽฅๅŒน้… JSON ๅฏน่ฑก๏ผˆๆฒกๆœ‰ไปฃ็ ๅ—ๅŒ…่ฃน็š„ๆƒ…ๅ†ต๏ผ‰
504
+ if not matches:
505
+ json_pattern = r'\{[^{}]*"name"\s*:\s*"[^"]+"\s*,\s*"arguments"\s*:\s*\{[^{}]*\}[^{}]*\}'
506
+ matches = re.findall(json_pattern, content, re.DOTALL)
507
+
508
+ for i, match in enumerate(matches):
509
+ try:
510
+ match = match.strip()
511
+ # ๅฐ่ฏ•่งฃๆž JSON
512
+ call_data = json.loads(match)
513
+ if call_data.get("name"):
514
+ tool_calls.append({
515
+ "id": f"call_{uuid.uuid4().hex[:8]}",
516
+ "type": "function",
517
+ "function": {
518
+ "name": call_data.get("name", ""),
519
+ "arguments": json.dumps(call_data.get("arguments", {}), ensure_ascii=False)
520
+ }
521
+ })
522
+ except json.JSONDecodeError:
523
+ continue
524
+
525
+ # ็งป้™คๅทฅๅ…ท่ฐƒ็”จ้ƒจๅˆ†
526
+ remaining = content
527
+ for pattern in patterns:
528
+ remaining = re.sub(pattern, '', remaining, flags=re.DOTALL)
529
+ remaining = remaining.strip()
530
+
531
+ return tool_calls, remaining
532
+
533
+
534
+ def load_config():
535
+ """
536
+ ๅŠ ่ฝฝ้…็ฝฎ๏ผŒไผ˜ๅ…ˆ็บง:
537
+ 1. config_data.json (ๅ‰็ซฏไฟๅญ˜็š„้…็ฝฎ)
538
+ 2. config.py (ๆœฌๅœฐๅผ€ๅ‘้…็ฝฎ๏ผŒไป…ไฝœไธบๅค‡็”จ)
539
+ """
540
+ global _config
541
+ loaded_from_json = False
542
+
543
+ # ไผ˜ๅ…ˆไปŽ JSON ๆ–‡ไปถๅŠ ่ฝฝ
544
+ if os.path.exists(CONFIG_FILE):
545
+ try:
546
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
547
+ saved = json.load(f)
548
+ if saved.get("SNLM0E") and saved.get("SECURE_1PSID"):
549
+ _config.update(saved)
550
+ loaded_from_json = True
551
+ except:
552
+ pass
553
+
554
+ # ๅฆ‚ๆžœ JSON ๆฒกๆœ‰ๆœ‰ๆ•ˆ้…็ฝฎ๏ผŒๅฐ่ฏ•ไปŽ config.py ๅŠ ่ฝฝ
555
+ if not loaded_from_json:
556
+ try:
557
+ import config
558
+ for key in _config:
559
+ if hasattr(config, key) and getattr(config, key):
560
+ _config[key] = getattr(config, key)
561
+ except:
562
+ pass
563
+
564
+
565
+ def save_config():
566
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
567
+ json.dump(_config, f, indent=2, ensure_ascii=False)
568
+
569
+
570
+ def get_client(auto_refresh: bool = True):
571
+ global _client, _last_token_refresh
572
+
573
+ if not _config.get("SNLM0E") or not _config.get("SECURE_1PSID"):
574
+ raise HTTPException(status_code=500, detail="่ฏทๅ…ˆๅœจๅŽๅฐ้…็ฝฎ Token ๅ’Œ Cookie")
575
+
576
+ # ๆฃ€ๆŸฅๆ˜ฏๅฆ้œ€่ฆ่‡ชๅŠจๅˆทๆ–ฐ token
577
+ if auto_refresh and TOKEN_AUTO_REFRESH:
578
+ current_time = time.time()
579
+ if (current_time - _last_token_refresh) >= TOKEN_REFRESH_INTERVAL_MIN:
580
+ try_refresh_tokens()
581
+
582
+ # ๅฆ‚ๆžœ client ๅทฒๅญ˜ๅœจ๏ผŒ็›ดๆŽฅๅค็”จ๏ผŒไฟๆŒไผš่ฏไธŠไธ‹ๆ–‡
583
+ if _client is not None:
584
+ return _client
585
+
586
+ cookies = f"__Secure-1PSID={_config['SECURE_1PSID']}"
587
+ if _config.get("SECURE_1PSIDTS"):
588
+ cookies += f"; __Secure-1PSIDTS={_config['SECURE_1PSIDTS']}"
589
+ if _config.get("SAPISID"):
590
+ cookies += f"; SAPISID={_config['SAPISID']}; __Secure-1PAPISID={_config['SAPISID']}"
591
+ if _config.get("SID"):
592
+ cookies += f"; SID={_config['SID']}"
593
+ if _config.get("HSID"):
594
+ cookies += f"; HSID={_config['HSID']}"
595
+ if _config.get("SSID"):
596
+ cookies += f"; SSID={_config['SSID']}"
597
+ if _config.get("APISID"):
598
+ cookies += f"; APISID={_config['APISID']}"
599
+
600
+ # ๆž„ๅปบๅช’ไฝ“ๆ–‡ไปถ็š„ๅŸบ็ก€ URL (ไผ˜ๅ…ˆไฝฟ็”จ้…็ฝฎ็š„ๅค–็ฝ‘ๅœฐๅ€)
601
+ media_base_url = MEDIA_BASE_URL if MEDIA_BASE_URL else f"http://localhost:{PORT}"
602
+
603
+ from client import GeminiClient
604
+ _client = GeminiClient(
605
+ secure_1psid=_config["SECURE_1PSID"],
606
+ snlm0e=_config["SNLM0E"],
607
+ cookies_str=cookies,
608
+ push_id=_config.get("PUSH_ID") or None,
609
+ model_ids=_config.get("MODEL_IDS") or DEFAULT_MODEL_IDS,
610
+ debug=False,
611
+ media_base_url=media_base_url,
612
+ )
613
+ return _client
614
+
615
+
616
+ def get_login_html():
617
+ return '''<!DOCTYPE html>
618
+ <html lang="zh-CN">
619
+ <head>
620
+ <meta charset="UTF-8">
621
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
622
+ <title>็™ปๅฝ• - Gemini API</title>
623
+ <style>
624
+ * { box-sizing: border-box; margin: 0; padding: 0; }
625
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
626
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh;
627
+ display: flex; align-items: center; justify-content: center; padding: 20px; }
628
+ .login-card { background: white; border-radius: 16px; padding: 40px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); width: 100%; max-width: 400px; }
629
+ h1 { color: #333; margin-bottom: 10px; font-size: 28px; text-align: center; }
630
+ .subtitle { color: #666; margin-bottom: 30px; font-size: 14px; text-align: center; }
631
+ .form-group { margin-bottom: 20px; }
632
+ label { display: block; font-size: 13px; font-weight: 500; color: #555; margin-bottom: 8px; }
633
+ input { width: 100%; padding: 14px 16px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 15px; transition: border-color 0.2s; }
634
+ input:focus { outline: none; border-color: #667eea; }
635
+ .btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 14px 30px;
636
+ border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; width: 100%; margin-top: 10px; transition: transform 0.2s, box-shadow 0.2s; }
637
+ .btn:hover { transform: translateY(-2px); box-shadow: 0 5px 20px rgba(102,126,234,0.4); }
638
+ .btn:disabled { opacity: 0.7; cursor: not-allowed; transform: none; }
639
+ .error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; display: none; }
640
+ .logo { text-align: center; margin-bottom: 20px; font-size: 48px; }
641
+ </style>
642
+ </head>
643
+ <body>
644
+ <div class="login-card">
645
+ <div class="logo">๐Ÿค–</div>
646
+ <h1>Gemini API</h1>
647
+ <p class="subtitle">่ฏท็™ปๅฝ•ไปฅ่ฎฟ้—ฎๅŽๅฐ็ฎก็†</p>
648
+
649
+ <div id="error" class="error"></div>
650
+
651
+ <form id="loginForm">
652
+ <div class="form-group">
653
+ <label>็”จๆˆทๅ</label>
654
+ <input type="text" name="username" id="username" placeholder="่ฏท่พ“ๅ…ฅ็”จๆˆทๅ" required autofocus>
655
+ </div>
656
+ <div class="form-group">
657
+ <label>ๅฏ†็ </label>
658
+ <input type="password" name="password" id="password" placeholder="่ฏท่พ“ๅ…ฅๅฏ†็ " required>
659
+ </div>
660
+ <button type="submit" class="btn" id="submitBtn">็™ป ๅฝ•</button>
661
+ </form>
662
+ </div>
663
+
664
+ <script>
665
+ document.getElementById('loginForm').addEventListener('submit', async (e) => {
666
+ e.preventDefault();
667
+ const errorEl = document.getElementById('error');
668
+ const submitBtn = document.getElementById('submitBtn');
669
+
670
+ errorEl.style.display = 'none';
671
+ submitBtn.disabled = true;
672
+ submitBtn.textContent = '็™ปๅฝ•ไธญ...';
673
+
674
+ try {
675
+ const resp = await fetch('/admin/login', {
676
+ method: 'POST',
677
+ headers: {'Content-Type': 'application/json'},
678
+ body: JSON.stringify({
679
+ username: document.getElementById('username').value,
680
+ password: document.getElementById('password').value
681
+ })
682
+ });
683
+ const result = await resp.json();
684
+
685
+ if (result.success) {
686
+ window.location.href = '/admin';
687
+ } else {
688
+ errorEl.textContent = result.message || '็™ปๅฝ•ๅคฑ่ดฅ';
689
+ errorEl.style.display = 'block';
690
+ }
691
+ } catch (err) {
692
+ errorEl.textContent = '็ฝ‘็ปœ้”™่ฏฏ: ' + err.message;
693
+ errorEl.style.display = 'block';
694
+ } finally {
695
+ submitBtn.disabled = false;
696
+ submitBtn.textContent = '็™ป ๅฝ•';
697
+ }
698
+ });
699
+ </script>
700
+ </body>
701
+ </html>'''
702
+
703
+
704
+ def get_admin_html():
705
+ return '''<!DOCTYPE html>
706
+ <html lang="zh-CN">
707
+ <head>
708
+ <meta charset="UTF-8">
709
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
710
+ <title>Gemini API ้…็ฝฎ</title>
711
+ <style>
712
+ * { box-sizing: border-box; margin: 0; padding: 0; }
713
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
714
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; }
715
+ .container { max-width: 800px; margin: 0 auto; }
716
+ .card { background: white; border-radius: 16px; padding: 30px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); position: relative; }
717
+ .token-status { position: absolute; top: 15px; left: 20px; font-size: 12px; padding: 6px 12px; border-radius: 20px; font-weight: 500; }
718
+ .token-status.valid { background: #d4edda; color: #155724; }
719
+ .token-status.invalid { background: #f8d7da; color: #721c24; }
720
+ .token-status.loading { background: #fff3cd; color: #856404; }
721
+ h1 { color: #333; margin-bottom: 10px; font-size: 28px; }
722
+ .subtitle { color: #666; margin-bottom: 30px; font-size: 14px; }
723
+ .section { margin-bottom: 25px; }
724
+ .section-title { font-size: 16px; font-weight: 600; color: #333; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #eee; }
725
+ .required { color: #e74c3c; }
726
+ .optional { color: #95a5a6; font-size: 12px; }
727
+ .form-group { margin-bottom: 15px; }
728
+ label { display: block; font-size: 13px; font-weight: 500; color: #555; margin-bottom: 5px; }
729
+ input, textarea { width: 100%; padding: 12px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; font-family: monospace; transition: border-color 0.2s; }
730
+ input:focus, textarea:focus { outline: none; border-color: #667eea; }
731
+ textarea { resize: vertical; min-height: 80px; }
732
+ .btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 14px 30px;
733
+ border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; width: 100%; margin-top: 20px; transition: transform 0.2s, box-shadow 0.2s; }
734
+ .btn:hover { transform: translateY(-2px); box-shadow: 0 5px 20px rgba(102,126,234,0.4); }
735
+ .status { margin-top: 20px; padding: 15px; border-radius: 8px; font-size: 14px; display: none; }
736
+ .status.success { background: #d4edda; color: #155724; display: block; }
737
+ .status.error { background: #f8d7da; color: #721c24; display: block; }
738
+ .info-box { background: #f8f9fa; border-radius: 8px; padding: 15px; margin-bottom: 20px; font-size: 13px; color: #666; }
739
+ .info-box code { background: #e9ecef; padding: 2px 6px; border-radius: 4px; }
740
+ .api-info { background: #e8f4fd; border-left: 4px solid #667eea; padding: 15px; margin-top: 20px; border-radius: 0 8px 8px 0; }
741
+ .api-info h3 { font-size: 14px; margin-bottom: 10px; color: #333; }
742
+ .api-info pre { background: #fff; padding: 10px; border-radius: 4px; font-size: 12px; margin-top: 5px; overflow-x: auto; }
743
+ .parsed-info { background: #f0f9ff; border: 1px solid #bae6fd; border-radius: 8px; padding: 15px; margin-top: 15px; font-size: 12px; display: none; }
744
+ .parsed-info h4 { color: #0369a1; margin-bottom: 10px; }
745
+ .parsed-info .item { margin: 5px 0; color: #555; }
746
+ .parsed-info .item span { color: #059669; font-family: monospace; }
747
+ </style>
748
+ </head>
749
+ <body>
750
+ <div class="container">
751
+ <div class="card">
752
+ <div id="tokenStatus" class="token-status loading">๐Ÿ”„ ๆฃ€ๆŸฅไธญ...</div>
753
+ <h1>๐Ÿค– Gemini API ้…็ฝฎ</h1>
754
+ <p class="subtitle">้…็ฝฎ Google Gemini ็š„่ฎค่ฏไฟกๆฏ๏ผŒไฟๅญ˜ๅŽๅณๅฏ่ฐƒ็”จ API <a href="/admin/logout" style="float:right;color:#667eea;text-decoration:none;">้€€ๅ‡บ็™ปๅฝ•</a></p>
755
+
756
+ <div class="info-box">
757
+ <strong>่Žทๅ–ๆ–นๆณ•๏ผš</strong><br>
758
+ 1. ๆ‰“ๅผ€ <a href="https://gemini.google.com" target="_blank">gemini.google.com</a> ๅนถ็™ปๅฝ•<br>
759
+ 2. F12 โ†’ ็ฝ‘็ปœ โ†’ ๅ‘้€ๅ†…ๅฎนๅˆฐ่Šๅคฉ โ†’ ็‚นๅ‡ปไปปๆ„่ฏทๆฑ‚ โ†’ Copy ่ฏทๆฑ‚ๅคดๅ†…ๅฎŒๆ•ดcookie
760
+ </div>
761
+
762
+ <form id="configForm">
763
+ <div class="section">
764
+ <div class="section-title">๐Ÿ”‘ Cookie ้…็ฝฎ</div>
765
+ <div class="form-group">
766
+ <label>ๅฎŒๆ•ด Cookie <span class="required">*</span></label>
767
+ <textarea name="FULL_COOKIE" id="FULL_COOKIE" rows="6" placeholder="็ฒ˜่ดดไปŽๆต่งˆๅ™จๅคๅˆถ็š„ๅฎŒๆ•ด Cookie ๅญ—็ฌฆไธฒ๏ผŒ็ณป็ปŸไผš่‡ชๅŠจ่งฃๆžๆ‰€้œ€ๅญ—ๆฎตๅ’Œ Token..." required></textarea>
768
+ <div id="parsedInfo" class="parsed-info">
769
+ <h4>โœ… ๅทฒ่งฃๆž็š„ๅญ—ๆฎต๏ผš</h4>
770
+ <div id="parsedFields"></div>
771
+ </div>
772
+ </div>
773
+ </div>
774
+
775
+ <div class="section">
776
+ <div class="section-title">๐ŸŽฏ ๆจกๅž‹ ID ้…็ฝฎ <span class="optional">(ๅฏ้€‰๏ผŒๅฆ‚ๆžœๆจกๅž‹ๅˆ‡ๆขๅคฑๆ•ˆ่ฏทๆ›ดๆ–ฐ)</span></div>
777
+ <div class="info-box">
778
+ <strong>่Žทๅ–ๆ–นๆณ•๏ผš</strong>F12 โ†’ Network โ†’ ๅœจ Gemini ไธญๅˆ‡ๆขๆจกๅž‹ๅ‘้€ๆถˆๆฏ โ†’ ๆ‰พๅˆฐ่ฏทๆฑ‚ๅคด <code>x-goog-ext-525001261-jspb</code> โ†’ ๅคๅˆถๆ•ดไธชๆ•ฐ็ป„ๅ€ผ็ฒ˜่ดดๅˆฐไธ‹ๆ–น่พ“ๅ…ฅๆก†
779
+ </div>
780
+ <div class="form-group">
781
+ <label>ๅฟซ้€Ÿ่งฃๆž <span class="optional">(็ฒ˜่ดด่ฏทๆฑ‚ๅคดๆ•ฐ็ป„่‡ชๅŠจๆๅ– ID)</span></label>
782
+ <input type="text" id="MODEL_ID_PARSER" placeholder='็ฒ˜่ดดๅฆ‚: [1,null,null,null,"56fdd199312815e2",null,null,0,[4],null,null,2]'>
783
+ <div id="parsedModelId" class="parsed-info" style="margin-top:10px;">
784
+ <h4>โœ… ๅทฒๆๅ–็š„ๆจกๅž‹ ID๏ผš</h4>
785
+ <div id="parsedModelIdValue"></div>
786
+ </div>
787
+ </div>
788
+ <div class="form-group">
789
+ <label>ๆž้€Ÿ็‰ˆ (Flash) ID</label>
790
+ <input type="text" name="MODEL_ID_FLASH" id="MODEL_ID_FLASH" placeholder="56fdd199312815e2">
791
+ </div>
792
+ <div class="form-group">
793
+ <label>Pro ็‰ˆ ID</label>
794
+ <input type="text" name="MODEL_ID_PRO" id="MODEL_ID_PRO" placeholder="e6fa609c3fa255c0">
795
+ </div>
796
+ <div class="form-group">
797
+ <label>ๆ€่€ƒ็‰ˆ (Thinking) ID</label>
798
+ <input type="text" name="MODEL_ID_THINKING" id="MODEL_ID_THINKING" placeholder="e051ce1aa80aa576">
799
+ </div>
800
+ </div>
801
+
802
+ <button type="submit" class="btn">๐Ÿ’พ ไฟๅญ˜้…็ฝฎ</button>
803
+ </form>
804
+
805
+ <div id="status" class="status"></div>
806
+
807
+ <div class="api-info">
808
+ <h3>๐Ÿ“ก API ่ฐƒ็”จไฟกๆฏ</h3>
809
+ <p>Base URL: <strong id="baseUrl"></strong></p>
810
+ <p>API Key: <strong id="apiKey"></strong></p>
811
+ <p>ๅฏ็”จๆจกๅž‹: <code>gemini-3.0-flash</code> | <code>gemini-3.0-pro</code> | <code>gemini-3.0-flash-thinking</code></p>
812
+
813
+ <h4 style="margin-top:15px;">๐Ÿ’ฌ ๆ–‡ๆœฌๅฏน่ฏ</h4>
814
+ <pre>from openai import OpenAI
815
+ client = OpenAI(base_url="<span id="codeUrl"></span>", api_key="<span id="codeKey"></span>")
816
+
817
+ response = client.chat.completions.create(
818
+ model="gemini-3.0-flash", # ๆˆ– gemini-3.0-pro / gemini-3.0-flash-thinking
819
+ messages=[{"role": "user", "content": "ไฝ ๅฅฝ"}]
820
+ )
821
+ print(response.choices[0].message.content)</pre>
822
+
823
+ <h4 style="margin-top:15px;">๐Ÿ–ผ๏ธ ๅ›พ็‰‡่ฏ†ๅˆซ</h4>
824
+ <pre>import base64
825
+ from openai import OpenAI
826
+ client = OpenAI(base_url="<span id="codeUrl2"></span>", api_key="<span id="codeKey2"></span>")
827
+
828
+ # ่ฏปๅ–ๆœฌๅœฐๅ›พ็‰‡
829
+ with open("image.png", "rb") as f:
830
+ img_b64 = base64.b64encode(f.read()).decode()
831
+
832
+ response = client.chat.completions.create(
833
+ model="gemini-3.0-flash",
834
+ messages=[{
835
+ "role": "user",
836
+ "content": [
837
+ {"type": "text", "text": "่ฏทๆ่ฟฐ่ฟ™ๅผ ๅ›พ็‰‡"},
838
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_b64}"}}
839
+ ]
840
+ }]
841
+ )
842
+ print(response.choices[0].message.content)</pre>
843
+
844
+ <h4 style="margin-top:15px;">๐ŸŒŠ ๆตๅผๅ“ๅบ”</h4>
845
+ <pre>stream = client.chat.completions.create(
846
+ model="gemini-3.0-flash",
847
+ messages=[{"role": "user", "content": "ๅ†™ไธ€้ฆ–่ฏ—"}],
848
+ stream=True
849
+ )
850
+ for chunk in stream:
851
+ if chunk.choices[0].delta.content:
852
+ print(chunk.choices[0].delta.content, end="", flush=True)</pre>
853
+
854
+ <h4 style="margin-top:15px;">๐Ÿ“ท ็คบไพ‹ๅ›พ็‰‡</h4>
855
+ <p style="font-size:12px;color:#666;">ไปฅไธ‹ๆ˜ฏ image.png ็คบไพ‹ๅ›พ็‰‡๏ผŒๅฏ็”จไบŽๆต‹่ฏ•ๅ›พ็‰‡่ฏ†ๅˆซๅŠŸ่ƒฝ๏ผˆ็‚นๅ‡ปๆ”พๅคง๏ผ‰๏ผš</p>
856
+ <img id="sampleImage" src="/static/image.png" alt="็คบไพ‹ๅ›พ็‰‡" style="max-width:300px;border-radius:8px;margin-top:10px;border:1px solid #ddd;cursor:pointer;" onclick="showImageModal()" onerror="this.style.display='none';this.nextElementSibling.style.display='block';">
857
+ <p style="display:none;font-size:12px;color:#999;">๏ผˆ็คบไพ‹ๅ›พ็‰‡ไธๅฏ็”จ๏ผŒ่ฏท็กฎไฟ image.png ๆ–‡ไปถๅญ˜ๅœจ๏ผ‰</p>
858
+ </div>
859
+ </div>
860
+ </div>
861
+
862
+ <!-- ๅ›พ็‰‡ๆ”พๅคงๆจกๆ€ๆก† -->
863
+ <div id="imageModal" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:1000;justify-content:center;align-items:center;cursor:pointer;" onclick="hideImageModal()">
864
+ <img src="/static/image.png" alt="็คบไพ‹ๅ›พ็‰‡" style="max-width:90%;max-height:90%;border-radius:8px;box-shadow:0 0 30px rgba(0,0,0,0.5);">
865
+ <span style="position:absolute;top:20px;right:30px;color:white;font-size:30px;cursor:pointer;">&times;</span>
866
+ </div>
867
+
868
+ <script>
869
+ // ๅ›พ็‰‡ๆ”พๅคงๅŠŸ่ƒฝ
870
+ function showImageModal() {
871
+ document.getElementById('imageModal').style.display = 'flex';
872
+ document.body.style.overflow = 'hidden';
873
+ }
874
+ function hideImageModal() {
875
+ document.getElementById('imageModal').style.display = 'none';
876
+ document.body.style.overflow = 'auto';
877
+ }
878
+ // ESC ้”ฎๅ…ณ้—ญ
879
+ document.addEventListener('keydown', (e) => {
880
+ if (e.key === 'Escape') hideImageModal();
881
+ });
882
+
883
+ const API_KEY = "''' + API_KEY + '''";
884
+ const PORT = ''' + str(PORT) + ''';
885
+
886
+ document.getElementById('baseUrl').textContent = 'http://localhost:' + PORT + '/v1';
887
+ document.getElementById('apiKey').textContent = API_KEY;
888
+ document.getElementById('codeUrl').textContent = 'http://localhost:' + PORT + '/v1';
889
+ document.getElementById('codeKey').textContent = API_KEY;
890
+ document.getElementById('codeUrl2').textContent = 'http://localhost:' + PORT + '/v1';
891
+ document.getElementById('codeKey2').textContent = API_KEY;
892
+
893
+ // ่Žทๅ–ๅนถๆ˜พ็คบ Token ็Šถๆ€
894
+ async function updateTokenStatus() {
895
+ const statusEl = document.getElementById('tokenStatus');
896
+ try {
897
+ const resp = await fetch('/v1/token/status', {
898
+ headers: { 'Authorization': 'Bearer ' + API_KEY }
899
+ });
900
+ const data = await resp.json();
901
+
902
+ if (data.has_snlm0e && data.total_refresh_count >= 0) {
903
+ statusEl.className = 'token-status valid';
904
+ statusEl.innerHTML = 'โœ… Token ๆœ‰ๆ•ˆ | ๅทฒๅˆทๆ–ฐ ' + data.total_refresh_count + ' ๆฌก';
905
+ } else {
906
+ statusEl.className = 'token-status invalid';
907
+ statusEl.innerHTML = 'โŒ Token ๅทฒๅคฑๆ•ˆ';
908
+ }
909
+ } catch (e) {
910
+ statusEl.className = 'token-status invalid';
911
+ statusEl.innerHTML = 'โŒ ๆ— ๆณ•่Žทๅ–็Šถๆ€';
912
+ }
913
+ }
914
+
915
+ // ้กต้ขๅŠ ่ฝฝๆ—ถ่Žทๅ–็Šถๆ€๏ผŒๅนถๆฏ 30 ็ง’ๅˆทๆ–ฐไธ€ๆฌก
916
+ updateTokenStatus();
917
+ setInterval(updateTokenStatus, 30000);
918
+
919
+ // ่งฃๆžๆจกๅž‹ ID (ไปŽ x-goog-ext-525001261-jspb ๆ•ฐ็ป„ไธญๆๅ–)
920
+ function parseModelId(input) {
921
+ try {
922
+ // ๅฐ่ฏ•่งฃๆž JSON ๆ•ฐ็ป„
923
+ const arr = JSON.parse(input);
924
+ if (Array.isArray(arr) && arr.length > 4 && typeof arr[4] === 'string') {
925
+ return arr[4];
926
+ }
927
+ } catch (e) {
928
+ // ๅฐ่ฏ•็”จๆญฃๅˆ™ๆๅ– 16 ไฝๅๅ…ญ่ฟ›ๅˆถๅญ—็ฌฆไธฒ
929
+ const match = input.match(/["\']([a-f0-9]{16})["\']/i);
930
+ if (match) {
931
+ return match[1];
932
+ }
933
+ }
934
+ return null;
935
+ }
936
+
937
+ // ็›‘ๅฌๆจกๅž‹ ID ่งฃๆž่พ“ๅ…ฅ
938
+ document.getElementById('MODEL_ID_PARSER').addEventListener('input', (e) => {
939
+ const modelId = parseModelId(e.target.value);
940
+ const container = document.getElementById('parsedModelIdValue');
941
+ const infoBox = document.getElementById('parsedModelId');
942
+
943
+ if (modelId) {
944
+ container.innerHTML = '<div class="item">ๆๅ–ๅˆฐ็š„ ID: <span style="color:#059669;font-family:monospace;">' + modelId + '</span></div>' +
945
+ '<div style="margin-top:10px;">' +
946
+ '<button type="button" onclick="fillModelId(\\'flash\\', \\'' + modelId + '\\')" style="margin-right:5px;padding:5px 10px;cursor:pointer;">ๅกซๅ…ฅๆž้€Ÿ็‰ˆ</button>' +
947
+ '<button type="button" onclick="fillModelId(\\'pro\\', \\'' + modelId + '\\')" style="margin-right:5px;padding:5px 10px;cursor:pointer;">ๅกซๅ…ฅPro็‰ˆ</button>' +
948
+ '<button type="button" onclick="fillModelId(\\'thinking\\', \\'' + modelId + '\\')" style="padding:5px 10px;cursor:pointer;">ๅกซๅ…ฅๆ€่€ƒ็‰ˆ</button>' +
949
+ '</div>';
950
+ infoBox.style.display = 'block';
951
+ } else {
952
+ infoBox.style.display = 'none';
953
+ }
954
+ });
955
+
956
+ // ๅกซๅ…ฅๆจกๅž‹ ID
957
+ function fillModelId(type, id) {
958
+ const fieldMap = {
959
+ 'flash': 'MODEL_ID_FLASH',
960
+ 'pro': 'MODEL_ID_PRO',
961
+ 'thinking': 'MODEL_ID_THINKING'
962
+ };
963
+ document.getElementById(fieldMap[type]).value = id;
964
+ }
965
+
966
+ // Cookie ๅญ—ๆฎตๆ˜ ๅฐ„
967
+ const cookieFields = {
968
+ '__Secure-1PSID': 'SECURE_1PSID',
969
+ '__Secure-1PSIDTS': 'SECURE_1PSIDTS',
970
+ 'SAPISID': 'SAPISID',
971
+ '__Secure-1PAPISID': 'SECURE_1PAPISID',
972
+ 'SID': 'SID',
973
+ 'HSID': 'HSID',
974
+ 'SSID': 'SSID',
975
+ 'APISID': 'APISID'
976
+ };
977
+
978
+ // ่งฃๆž Cookie ๅญ—็ฌฆไธฒ
979
+ function parseCookie(cookieStr) {
980
+ const result = {};
981
+ if (!cookieStr) return result;
982
+
983
+ cookieStr.split(';').forEach(item => {
984
+ const trimmed = item.trim();
985
+ const eqIndex = trimmed.indexOf('=');
986
+ if (eqIndex > 0) {
987
+ const key = trimmed.substring(0, eqIndex).trim();
988
+ const value = trimmed.substring(eqIndex + 1).trim();
989
+ if (cookieFields[key]) {
990
+ result[cookieFields[key]] = value;
991
+ }
992
+ }
993
+ });
994
+ return result;
995
+ }
996
+
997
+ // ๆ˜พ็คบ่งฃๆž็ป“ๆžœ
998
+ function showParsedFields(parsed) {
999
+ const container = document.getElementById('parsedFields');
1000
+ const infoBox = document.getElementById('parsedInfo');
1001
+
1002
+ const fieldNames = {
1003
+ 'SECURE_1PSID': '__Secure-1PSID',
1004
+ 'SECURE_1PSIDTS': '__Secure-1PSIDTS',
1005
+ 'SAPISID': 'SAPISID',
1006
+ 'SID': 'SID',
1007
+ 'HSID': 'HSID',
1008
+ 'SSID': 'SSID',
1009
+ 'APISID': 'APISID'
1010
+ };
1011
+
1012
+ let html = '';
1013
+ let hasFields = false;
1014
+ for (const [key, name] of Object.entries(fieldNames)) {
1015
+ if (parsed[key]) {
1016
+ hasFields = true;
1017
+ const shortValue = parsed[key].length > 30 ? parsed[key].substring(0, 30) + '...' : parsed[key];
1018
+ html += '<div class="item">' + name + ': <span>' + shortValue + '</span></div>';
1019
+ }
1020
+ }
1021
+
1022
+ if (hasFields) {
1023
+ container.innerHTML = html;
1024
+ infoBox.style.display = 'block';
1025
+ } else {
1026
+ infoBox.style.display = 'none';
1027
+ }
1028
+ }
1029
+
1030
+ // ็›‘ๅฌ Cookie ่พ“ๅ…ฅ
1031
+ document.getElementById('FULL_COOKIE').addEventListener('input', (e) => {
1032
+ const parsed = parseCookie(e.target.value);
1033
+ showParsedFields(parsed);
1034
+ });
1035
+
1036
+ // ๅŠ ่ฝฝ้…็ฝฎ
1037
+ fetch('/admin/config', {credentials: 'same-origin'}).then(r => {
1038
+ if (!r.ok) throw new Error('ๆœช็™ปๅฝ•');
1039
+ return r.json();
1040
+ }).then(config => {
1041
+ if (config.FULL_COOKIE) {
1042
+ document.getElementById('FULL_COOKIE').value = config.FULL_COOKIE;
1043
+ showParsedFields(parseCookie(config.FULL_COOKIE));
1044
+ }
1045
+ // ๅŠ ่ฝฝๆจกๅž‹ ID
1046
+ if (config.MODEL_IDS) {
1047
+ document.getElementById('MODEL_ID_FLASH').value = config.MODEL_IDS.flash || '';
1048
+ document.getElementById('MODEL_ID_PRO').value = config.MODEL_IDS.pro || '';
1049
+ document.getElementById('MODEL_ID_THINKING').value = config.MODEL_IDS.thinking || '';
1050
+ }
1051
+ }).catch(err => {
1052
+ console.log('ๅŠ ่ฝฝ้…็ฝฎๅคฑ่ดฅ:', err);
1053
+ });
1054
+
1055
+ document.getElementById('configForm').addEventListener('submit', async (e) => {
1056
+ e.preventDefault();
1057
+ const formData = new FormData(e.target);
1058
+ const data = Object.fromEntries(formData.entries());
1059
+
1060
+ // ๆž„ๅปบๆจกๅž‹ ID ๅฏน่ฑก
1061
+ data.MODEL_IDS = {
1062
+ flash: data.MODEL_ID_FLASH || '',
1063
+ pro: data.MODEL_ID_PRO || '',
1064
+ thinking: data.MODEL_ID_THINKING || ''
1065
+ };
1066
+ delete data.MODEL_ID_FLASH;
1067
+ delete data.MODEL_ID_PRO;
1068
+ delete data.MODEL_ID_THINKING;
1069
+
1070
+ const statusEl = document.getElementById('status');
1071
+ statusEl.className = 'status';
1072
+ statusEl.style.display = 'none';
1073
+ statusEl.textContent = '';
1074
+
1075
+ // ๆ˜พ็คบไฟๅญ˜ไธญ็Šถๆ€
1076
+ const submitBtn = e.target.querySelector('button[type="submit"]');
1077
+ const originalText = submitBtn.textContent;
1078
+ submitBtn.textContent = 'โณ ไฟๅญ˜ไธญ...';
1079
+ submitBtn.disabled = true;
1080
+
1081
+ try {
1082
+ const resp = await fetch('/admin/save', {
1083
+ method: 'POST',
1084
+ headers: {'Content-Type': 'application/json'},
1085
+ credentials: 'same-origin',
1086
+ body: JSON.stringify(data)
1087
+ });
1088
+
1089
+ if (resp.status === 401) {
1090
+ window.location.href = '/admin/login';
1091
+ return;
1092
+ }
1093
+
1094
+ const result = await resp.json();
1095
+
1096
+ if (result.success) {
1097
+ statusEl.className = 'status success';
1098
+ statusEl.innerHTML = 'โœ… ' + result.message + '<br><br>๐Ÿ’ก <strong>้…็ฝฎๅทฒ็”Ÿๆ•ˆ๏ผŒๆ— ้œ€้‡ๅฏๆœๅŠก๏ผ</strong>';
1099
+ } else {
1100
+ statusEl.className = 'status error';
1101
+ statusEl.textContent = 'โŒ ' + result.message;
1102
+ }
1103
+ statusEl.style.display = 'block';
1104
+ } catch (err) {
1105
+ statusEl.className = 'status error';
1106
+ statusEl.textContent = 'โŒ ไฟๅญ˜ๅคฑ่ดฅ: ' + err.message;
1107
+ statusEl.style.display = 'block';
1108
+ } finally {
1109
+ submitBtn.textContent = originalText;
1110
+ submitBtn.disabled = false;
1111
+ }
1112
+ });
1113
+ </script>
1114
+ </body>
1115
+ </html>'''
1116
+
1117
+
1118
+ @app.get("/admin/login", response_class=HTMLResponse)
1119
+ async def admin_login_page():
1120
+ return get_login_html()
1121
+
1122
+
1123
+ @app.post("/admin/login")
1124
+ async def admin_login(request: Request):
1125
+ data = await request.json()
1126
+ username = data.get("username", "")
1127
+ password = data.get("password", "")
1128
+
1129
+ if username == ADMIN_USERNAME and password == ADMIN_PASSWORD:
1130
+ token = generate_session_token()
1131
+ _admin_sessions.add(token)
1132
+ response = JSONResponse({"success": True, "message": "็™ปๅฝ•ๆˆๅŠŸ"})
1133
+ response.set_cookie(key="admin_session", value=token, httponly=True, max_age=86400)
1134
+ return response
1135
+ else:
1136
+ return {"success": False, "message": "็”จๆˆทๅๆˆ–ๅฏ†็ ้”™่ฏฏ"}
1137
+
1138
+
1139
+ @app.get("/admin/logout")
1140
+ async def admin_logout(request: Request):
1141
+ token = request.cookies.get("admin_session")
1142
+ if token and token in _admin_sessions:
1143
+ _admin_sessions.discard(token)
1144
+ response = RedirectResponse(url="/admin/login", status_code=302)
1145
+ response.delete_cookie("admin_session")
1146
+ return response
1147
+
1148
+
1149
+ @app.get("/admin", response_class=HTMLResponse)
1150
+ async def admin_page(request: Request):
1151
+ if not verify_admin_session(request):
1152
+ return RedirectResponse(url="/admin/login", status_code=302)
1153
+ return get_admin_html()
1154
+
1155
+
1156
+ @app.post("/admin/save")
1157
+ async def admin_save(request: Request):
1158
+ if not verify_admin_session(request):
1159
+ raise HTTPException(status_code=401, detail="ๆœช็™ปๅฝ•")
1160
+
1161
+ global _client
1162
+ data = await request.json()
1163
+
1164
+ # ๅค„็†ๅฎŒๆ•ด Cookie ๅญ—็ฌฆไธฒ๏ผŒๅŽป้™คๅ‰ๅŽ็ฉบๆ ผ
1165
+ full_cookie = data.get("FULL_COOKIE", "").strip()
1166
+ if not full_cookie:
1167
+ return {"success": False, "message": "Cookie ๆ˜ฏๅฟ…ๅกซ้กน"}
1168
+
1169
+ # ่งฃๆž Cookie ๅญ—็ฌฆไธฒ
1170
+ parsed = parse_cookie_string(full_cookie)
1171
+
1172
+ if not parsed.get("SECURE_1PSID"):
1173
+ return {"success": False, "message": "Cookie ไธญๆœชๆ‰พๅˆฐ __Secure-1PSID ๅญ—ๆฎต๏ผŒ่ฏท็กฎไฟๅคๅˆถไบ†ๅฎŒๆ•ด็š„ Cookie"}
1174
+
1175
+ # ไปŽ้กต้ข่‡ชๅŠจ่Žทๅ– SNLM0E ๅ’Œ PUSH_ID
1176
+ tokens = fetch_tokens_from_page(full_cookie)
1177
+
1178
+ if not tokens.get("snlm0e"):
1179
+ return {"success": False, "message": "ๆ— ๆณ•่‡ชๅŠจ่Žทๅ– AT Token๏ผŒ่ฏทๆฃ€ๆŸฅ Cookie ๆ˜ฏๅฆๆœ‰ๆ•ˆๆˆ–ๅทฒ่ฟ‡ๆœŸ"}
1180
+
1181
+ # ๆ›ดๆ–ฐ้…็ฝฎ
1182
+ _config["FULL_COOKIE"] = full_cookie
1183
+ _config["SNLM0E"] = tokens["snlm0e"]
1184
+ _config["PUSH_ID"] = tokens.get("push_id", "")
1185
+
1186
+ # ไปŽ่งฃๆž็ป“ๆžœๆ›ดๆ–ฐๅ„ๅญ—ๆฎต
1187
+ for field in ["SECURE_1PSID", "SECURE_1PSIDTS", "SAPISID", "SID", "HSID", "SSID", "APISID"]:
1188
+ _config[field] = parsed.get(field, "")
1189
+
1190
+ # ไฝฟ็”จ่‡ชๅŠจ่Žทๅ–็š„ๆจกๅž‹ๅˆ—่กจ๏ผŒๅฆ‚ๆžœ่Žทๅ–ๅคฑ่ดฅๅˆ™ไฝฟ็”จ้ป˜่ฎคๅ€ผ
1191
+ if tokens.get("models"):
1192
+ _config["MODELS"] = tokens["models"]
1193
+ else:
1194
+ _config["MODELS"] = DEFAULT_MODELS.copy()
1195
+
1196
+ # ๅค„็†ๆจกๅž‹ ID ้…็ฝฎ
1197
+ model_ids = data.get("MODEL_IDS", {})
1198
+ if model_ids:
1199
+ # ๅชๆ›ดๆ–ฐ้ž็ฉบ็š„ๅ€ผ
1200
+ if model_ids.get("flash"):
1201
+ _config["MODEL_IDS"]["flash"] = model_ids["flash"]
1202
+ if model_ids.get("pro"):
1203
+ _config["MODEL_IDS"]["pro"] = model_ids["pro"]
1204
+ if model_ids.get("thinking"):
1205
+ _config["MODEL_IDS"]["thinking"] = model_ids["thinking"]
1206
+
1207
+ save_config()
1208
+ _client = None
1209
+
1210
+ # ๆž„ๅปบ็ป“ๆžœไฟกๆฏ
1211
+ parsed_fields = [k for k in ["SECURE_1PSID", "SECURE_1PSIDTS", "SAPISID", "SID", "HSID", "SSID", "APISID"] if parsed.get(k)]
1212
+ push_id_msg = f"๏ผŒPUSH_ID โœ“" if tokens.get("push_id") else "๏ผŒPUSH_ID โœ— (ๅ›พ็‰‡ๅŠŸ่ƒฝไธๅฏ็”จ)"
1213
+ models_msg = f"๏ผŒ{len(_config['MODELS'])} ไธชๆจกๅž‹" if _config.get("MODELS") else ""
1214
+
1215
+ try:
1216
+ get_client()
1217
+ return {
1218
+ "success": True,
1219
+ "message": f"้…็ฝฎๅทฒไฟๅญ˜ๅนถ้ชŒ่ฏๆˆๅŠŸ๏ผAT Token โœ“{push_id_msg}{models_msg}",
1220
+ "need_restart": False
1221
+ }
1222
+ except Exception as e:
1223
+ return {
1224
+ "success": True,
1225
+ "message": f"้…็ฝฎๅทฒไฟๅญ˜๏ผŒไฝ†่ฟžๆŽฅๆต‹่ฏ•ๅคฑ่ดฅ: {str(e)[:50]}",
1226
+ "need_restart": False
1227
+ }
1228
+
1229
+
1230
+ @app.get("/admin/config")
1231
+ async def admin_get_config(request: Request):
1232
+ if not verify_admin_session(request):
1233
+ raise HTTPException(status_code=401, detail="ๆœช็™ปๅฝ•")
1234
+ return _config
1235
+
1236
+
1237
+ # ============ API ่ทฏ็”ฑ ============
1238
+
1239
+ class ChatMessage(BaseModel):
1240
+ role: str
1241
+ content: Union[str, List[Dict[str, Any]]]
1242
+ name: Optional[str] = None
1243
+
1244
+ class Config:
1245
+ extra = "ignore"
1246
+
1247
+
1248
+ class FunctionDefinition(BaseModel):
1249
+ name: str
1250
+ description: Optional[str] = None
1251
+ parameters: Optional[Dict[str, Any]] = None
1252
+
1253
+ class ToolDefinition(BaseModel):
1254
+ type: str = "function"
1255
+ function: FunctionDefinition
1256
+
1257
+ class ChatCompletionRequest(BaseModel):
1258
+ model: str = "gemini"
1259
+ messages: List[ChatMessage]
1260
+ stream: Optional[bool] = False
1261
+ # Tools ๆ”ฏๆŒ
1262
+ tools: Optional[List[ToolDefinition]] = None
1263
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None
1264
+ # OpenAI SDK ๅฏ่ƒฝๅ‘้€็š„้ขๅค–ๅญ—ๆฎต
1265
+ temperature: Optional[float] = None
1266
+ max_tokens: Optional[int] = None
1267
+ top_p: Optional[float] = None
1268
+ frequency_penalty: Optional[float] = None
1269
+ presence_penalty: Optional[float] = None
1270
+ stop: Optional[Union[str, List[str]]] = None
1271
+ n: Optional[int] = None
1272
+ user: Optional[str] = None
1273
+
1274
+ class Config:
1275
+ extra = "ignore" # ๅฟฝ็•ฅๆœชๅฎšไน‰็š„้ขๅค–ๅญ—ๆฎต
1276
+
1277
+
1278
+ class ChatCompletionChoice(BaseModel):
1279
+ index: int
1280
+ message: Dict[str, Any]
1281
+ finish_reason: str
1282
+
1283
+
1284
+ class Usage(BaseModel):
1285
+ prompt_tokens: int
1286
+ completion_tokens: int
1287
+ total_tokens: int
1288
+
1289
+
1290
+ class ChatCompletionResponse(BaseModel):
1291
+ id: str
1292
+ object: str = "chat.completion"
1293
+ created: int
1294
+ model: str
1295
+ choices: List[ChatCompletionChoice]
1296
+ usage: Usage
1297
+
1298
+
1299
+ def verify_api_key(authorization: str = Header(None)):
1300
+ if not API_KEY:
1301
+ return True
1302
+ if not authorization or not authorization.startswith("Bearer ") or authorization[7:] != API_KEY:
1303
+ raise HTTPException(status_code=401, detail="Invalid API key")
1304
+ return True
1305
+
1306
+
1307
+ @app.get("/")
1308
+ async def root():
1309
+ return RedirectResponse(url="/admin")
1310
+
1311
+
1312
+ @app.get("/v1/models")
1313
+ async def list_models(authorization: str = Header(None)):
1314
+ verify_api_key(authorization)
1315
+ models = _config.get("MODELS", DEFAULT_MODELS)
1316
+ created = int(time.time())
1317
+ return {
1318
+ "object": "list",
1319
+ "data": [{"id": m, "object": "model", "created": created, "owned_by": "google"} for m in models]
1320
+ }
1321
+
1322
+
1323
+ @app.post("/v1/token/refresh")
1324
+ async def refresh_token_api(authorization: str = Header(None)):
1325
+ """ๆ‰‹ๅŠจๅˆทๆ–ฐ token API"""
1326
+ verify_api_key(authorization)
1327
+ result = try_refresh_tokens(force=True)
1328
+ return {
1329
+ "success": result["success"],
1330
+ "message": result["message"],
1331
+ "snlm0e_updated": bool(result.get("snlm0e")),
1332
+ "push_id_updated": bool(result.get("push_id")),
1333
+ "refresh_count": _token_refresh_count,
1334
+ }
1335
+
1336
+
1337
+ @app.get("/v1/token/status")
1338
+ async def token_status_api(authorization: str = Header(None)):
1339
+ """๏ฟฝ๏ฟฝ็œ‹ token ็Šถๆ€ API"""
1340
+ verify_api_key(authorization)
1341
+ current_time = time.time()
1342
+ time_since_refresh = int(current_time - _last_token_refresh) if _last_token_refresh > 0 else -1
1343
+
1344
+ return {
1345
+ "auto_refresh_enabled": TOKEN_AUTO_REFRESH,
1346
+ "background_refresh_enabled": TOKEN_BACKGROUND_REFRESH,
1347
+ "refresh_interval_range": f"{TOKEN_REFRESH_INTERVAL_MIN}-{TOKEN_REFRESH_INTERVAL_MAX}",
1348
+ "last_refresh_seconds_ago": time_since_refresh,
1349
+ "total_refresh_count": _token_refresh_count,
1350
+ "has_snlm0e": bool(_config.get("SNLM0E")),
1351
+ "has_push_id": bool(_config.get("PUSH_ID")),
1352
+ "client_active": _client is not None,
1353
+ }
1354
+
1355
+
1356
+ @app.post("/v1/client/reset")
1357
+ async def reset_client_api(authorization: str = Header(None)):
1358
+ """้‡็ฝฎ client API๏ผŒ็”จไบŽ token ๆ›ดๆ–ฐๅŽๅผบๅˆถ้‡ๆ–ฐๅˆ›ๅปบ client"""
1359
+ verify_api_key(authorization)
1360
+ reset_client()
1361
+ return {"success": True, "message": "Client ๅทฒ้‡็ฝฎ๏ผŒไธ‹ๆฌก่ฏทๆฑ‚ๅฐ†ไฝฟ็”จๆ–ฐ้…็ฝฎ"}
1362
+
1363
+
1364
+ def log_api_call(request_data: dict, response_data: dict, error: str = None):
1365
+ """่ฎฐๅฝ• API ่ฐƒ็”จๆ—ฅๅฟ—ๅˆฐๆ–‡ไปถ"""
1366
+ import datetime
1367
+ log_entry = {
1368
+ "timestamp": datetime.datetime.now().isoformat(),
1369
+ "request": request_data,
1370
+ "response": response_data,
1371
+ "error": error
1372
+ }
1373
+ try:
1374
+ with open("api_logs.json", "a", encoding="utf-8") as f:
1375
+ f.write(json.dumps(log_entry, ensure_ascii=False, indent=2) + "\n---\n")
1376
+ except Exception as e:
1377
+ print(f"[LOG ERROR] ๅ†™ๅ…ฅๆ—ฅๅฟ—ๅคฑ่ดฅ: {e}")
1378
+
1379
+
1380
+ # ็”จไบŽ่ฟฝ่ธชไผš่ฏ๏ผšไฟๅญ˜ไธŠๆฌก่ฏทๆฑ‚็š„ๆ‰€ๆœ‰็”จๆˆทๆถˆๆฏๅ†…ๅฎน
1381
+ _last_user_messages_hash = ""
1382
+
1383
+
1384
+ def get_user_messages_hash(messages: list) -> str:
1385
+ """่ฎก็ฎ—ๆ‰€ๆœ‰็”จๆˆทๆถˆๆฏ็š„ hash๏ผŒ็”จไบŽๅˆคๆ–ญๆ˜ฏๅฆๆ˜ฏๅŒไธ€ไผš่ฏ"""
1386
+ content_str = ""
1387
+ for m in messages:
1388
+ role = m.role if hasattr(m, 'role') else m.get('role', '')
1389
+ if role != "user":
1390
+ continue
1391
+ content = m.content if hasattr(m, 'content') else m.get('content', '')
1392
+ if isinstance(content, list):
1393
+ # ๅฏนไบŽๅŒ…ๅซๅ›พ็‰‡็š„ๆถˆๆฏ๏ผŒๅชๅ–ๆ–‡ๆœฌ้ƒจๅˆ†
1394
+ text_parts = [item.get('text', '') for item in content if item.get('type') == 'text']
1395
+ content_str += f"{' '.join(text_parts)}|"
1396
+ else:
1397
+ content_str += f"{content}|"
1398
+ return hashlib.md5(content_str.encode()).hexdigest()
1399
+
1400
+
1401
+ def is_continuation(current_messages: list, last_hash: str) -> bool:
1402
+ """
1403
+ ๅˆคๆ–ญๅฝ“ๅ‰่ฏทๆฑ‚ๆ˜ฏๅฆๆ˜ฏไธŠไธ€ๆฌกๅฏน่ฏ็š„ๅปถ็ปญ
1404
+
1405
+ ้€ป่พ‘๏ผšๅฆ‚ๆžœๅฝ“ๅ‰ๆถˆๆฏๅŽปๆމๆœ€ๅŽไธ€ๆก็”จๆˆทๆถˆๆฏๅŽ็š„ hash ็ญ‰ไบŽไธŠๆฌก็š„ hash๏ผŒ
1406
+ ่ฏดๆ˜Žๆ˜ฏๅŒไธ€ๅฏน่ฏ็š„ๅปถ็ปญ
1407
+ """
1408
+ if not last_hash:
1409
+ return False
1410
+
1411
+ # ๆ‰พๅˆฐๆ‰€ๆœ‰็”จๆˆทๆถˆๆฏ
1412
+ user_indices = [i for i, m in enumerate(current_messages)
1413
+ if (m.role if hasattr(m, 'role') else m.get('role', '')) == "user"]
1414
+
1415
+ if len(user_indices) <= 1:
1416
+ # ๅชๆœ‰ไธ€ๆก็”จๆˆทๆถˆๆฏ๏ผŒๆ— ๆณ•ๅˆคๆ–ญๆ˜ฏๅฆๅปถ็ปญ๏ผŒ่ง†ไธบๆ–ฐๅฏน่ฏ
1417
+ return False
1418
+
1419
+ # ๅŽปๆމๆœ€ๅŽไธ€ๆก็”จๆˆทๆถˆๆฏ๏ผŒ่ฎก็ฎ—ๅ‰ฉไฝ™ๆถˆๆฏ็š„ hash
1420
+ last_user_idx = user_indices[-1]
1421
+ prev_messages = current_messages[:last_user_idx]
1422
+ prev_hash = get_user_messages_hash(prev_messages)
1423
+
1424
+ return prev_hash == last_hash
1425
+
1426
+
1427
+ @app.post("/v1/chat/completions")
1428
+ async def chat_completions(request: ChatCompletionRequest, authorization: str = Header(None)):
1429
+ global _last_user_messages_hash
1430
+ verify_api_key(authorization)
1431
+
1432
+ # ่ฎฐๅฝ•่ฏทๆฑ‚ๅ…ฅๅ‚ (ๅ›พ็‰‡ๅ†…ๅฎนๆˆชๆ–ญๆ˜พ็คบ)
1433
+ request_log = {
1434
+ "model": request.model,
1435
+ "stream": request.stream,
1436
+ "messages": [],
1437
+ "tools": [t.model_dump() for t in request.tools] if request.tools else None
1438
+ }
1439
+ image_count = 0
1440
+ for m in request.messages:
1441
+ msg_log = {"role": m.role}
1442
+ if isinstance(m.content, list):
1443
+ content_log = []
1444
+ for item in m.content:
1445
+ if item.get("type") == "image_url":
1446
+ image_count += 1
1447
+ img_url = item.get("image_url", {})
1448
+ if isinstance(img_url, dict):
1449
+ url = img_url.get("url", "")
1450
+ else:
1451
+ url = str(img_url)
1452
+ # ๅˆคๆ–ญๅ›พ็‰‡ๆ ผๅผ
1453
+ if url.startswith("data:"):
1454
+ img_format = "base64"
1455
+ elif url.startswith("http://") or url.startswith("https://"):
1456
+ img_format = "url"
1457
+ else:
1458
+ img_format = "unknown"
1459
+ content_log.append({
1460
+ "type": "image_url",
1461
+ "format": img_format,
1462
+ "url_preview": url[:100] + "..." if len(url) > 100 else url
1463
+ })
1464
+ else:
1465
+ content_log.append(item)
1466
+ msg_log["content"] = content_log
1467
+ else:
1468
+ msg_log["content"] = m.content
1469
+ request_log["messages"].append(msg_log)
1470
+
1471
+ # ๆ‰“ๅฐๅ›พ็‰‡ๆŽฅๆ”ถๆƒ…ๅ†ต
1472
+ if image_count > 0:
1473
+ print(f"๐Ÿ“ท ๆ”ถๅˆฐ {image_count} ๅผ ๅ›พ็‰‡")
1474
+
1475
+ try:
1476
+ client = get_client()
1477
+
1478
+ if not is_continuation(request.messages, _last_user_messages_hash):
1479
+ client.reset()
1480
+
1481
+ # ๅค„็†ๆถˆๆฏ
1482
+ messages = []
1483
+ for m in request.messages:
1484
+ content = m.content
1485
+ if isinstance(content, list):
1486
+ messages.append({"role": m.role, "content": content})
1487
+ else:
1488
+ messages.append({"role": m.role, "content": content})
1489
+
1490
+ # ๅฆ‚ๆžœๆœ‰ tools๏ผŒๆŠŠๅทฅๅ…ทๆ็คบ่ฏ็›ดๆŽฅๅŠ ๅˆฐ็”จๆˆทๆถˆๆฏๅ‰้ข
1491
+ if request.tools and len(messages) > 0:
1492
+ tools_prompt = build_tools_prompt([t.model_dump() for t in request.tools])
1493
+ for i in range(len(messages) - 1, -1, -1):
1494
+ if messages[i]["role"] == "user":
1495
+ original = messages[i]["content"]
1496
+ if isinstance(original, str):
1497
+ messages[i]["content"] = tools_prompt + original
1498
+ break
1499
+
1500
+ if request.stream:
1501
+ _last_user_messages_hash = get_user_messages_hash(request.messages)
1502
+ stream_iter = client.chat(messages=messages, model=request.model, stream=True)
1503
+
1504
+ def generate_real_stream():
1505
+ streamed_text_parts = []
1506
+ last_chunk = None
1507
+
1508
+ for chunk in stream_iter:
1509
+ if not chunk:
1510
+ continue
1511
+ last_chunk = chunk
1512
+
1513
+ try:
1514
+ choices = chunk.get("choices", [])
1515
+ if choices:
1516
+ delta = choices[0].get("delta", {}) or {}
1517
+ piece = delta.get("content")
1518
+ if isinstance(piece, str) and piece:
1519
+ streamed_text_parts.append(piece)
1520
+ except Exception:
1521
+ pass
1522
+
1523
+ yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
1524
+
1525
+ # ๆตๅผไนŸ่ฎฐๅฝ•ๆœ€็ปˆๅ“ๅบ”ๆ‘˜่ฆ๏ผŒไพฟไบŽๆ—ฅๅฟ—ๆŽ’ๆŸฅ
1526
+ try:
1527
+ full_stream_text = "".join(streamed_text_parts)
1528
+ completion_id_log = (last_chunk or {}).get("id", f"chatcmpl-{uuid.uuid4().hex[:8]}")
1529
+ created_time_log = (last_chunk or {}).get("created", int(time.time()))
1530
+ response_log = {
1531
+ "id": completion_id_log,
1532
+ "object": "chat.completion",
1533
+ "created": created_time_log,
1534
+ "model": request.model,
1535
+ "choices": [{
1536
+ "index": 0,
1537
+ "message": {"role": "assistant", "content": full_stream_text},
1538
+ "finish_reason": "stop"
1539
+ }],
1540
+ "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
1541
+ }
1542
+ log_api_call(request_log, response_log)
1543
+ except Exception:
1544
+ pass
1545
+
1546
+ yield "data: [DONE]\n\n"
1547
+
1548
+ return StreamingResponse(
1549
+ generate_real_stream(),
1550
+ media_type="text/event-stream",
1551
+ headers={
1552
+ "Cache-Control": "no-cache",
1553
+ "Connection": "keep-alive",
1554
+ "X-Accel-Buffering": "no",
1555
+ }
1556
+ )
1557
+
1558
+ response = client.chat(messages=messages, model=request.model)
1559
+ _last_user_messages_hash = get_user_messages_hash(request.messages)
1560
+
1561
+ reply_content = response.choices[0].message.content
1562
+ completion_id = f"chatcmpl-{uuid.uuid4().hex[:8]}"
1563
+ created_time = int(time.time())
1564
+
1565
+ # ่งฃๆžๅทฅๅ…ท่ฐƒ็”จ
1566
+ tool_calls = []
1567
+ final_content = reply_content
1568
+ if request.tools:
1569
+ tool_calls, final_content = parse_tool_calls(reply_content)
1570
+
1571
+ # ๅค„็†ๆตๅผๅ“ๅบ”
1572
+ if request.stream:
1573
+ async def generate_stream():
1574
+ chunk_data = {
1575
+ "id": completion_id,
1576
+ "object": "chat.completion.chunk",
1577
+ "created": created_time,
1578
+ "model": request.model,
1579
+ "choices": [{
1580
+ "index": 0,
1581
+ "delta": {"role": "assistant"},
1582
+ "finish_reason": None
1583
+ }]
1584
+ }
1585
+ yield f"data: {json.dumps(chunk_data)}\n\n"
1586
+
1587
+ if tool_calls:
1588
+ # ๆตๅผ่ฟ”ๅ›žๅทฅๅ…ท่ฐƒ็”จ
1589
+ for tc in tool_calls:
1590
+ chunk_data = {
1591
+ "id": completion_id,
1592
+ "object": "chat.completion.chunk",
1593
+ "created": created_time,
1594
+ "model": request.model,
1595
+ "choices": [{
1596
+ "index": 0,
1597
+ "delta": {"tool_calls": [tc]},
1598
+ "finish_reason": None
1599
+ }]
1600
+ }
1601
+ yield f"data: {json.dumps(chunk_data)}\n\n"
1602
+ else:
1603
+ chunk_data = {
1604
+ "id": completion_id,
1605
+ "object": "chat.completion.chunk",
1606
+ "created": created_time,
1607
+ "model": request.model,
1608
+ "choices": [{
1609
+ "index": 0,
1610
+ "delta": {"content": final_content},
1611
+ "finish_reason": None
1612
+ }]
1613
+ }
1614
+ yield f"data: {json.dumps(chunk_data)}\n\n"
1615
+
1616
+ chunk_data = {
1617
+ "id": completion_id,
1618
+ "object": "chat.completion.chunk",
1619
+ "created": created_time,
1620
+ "model": request.model,
1621
+ "choices": [{
1622
+ "index": 0,
1623
+ "delta": {},
1624
+ "finish_reason": "tool_calls" if tool_calls else "stop"
1625
+ }]
1626
+ }
1627
+ yield f"data: {json.dumps(chunk_data)}\n\n"
1628
+ yield "data: [DONE]\n\n"
1629
+
1630
+ return StreamingResponse(
1631
+ generate_stream(),
1632
+ media_type="text/event-stream",
1633
+ headers={
1634
+ "Cache-Control": "no-cache",
1635
+ "Connection": "keep-alive",
1636
+ "X-Accel-Buffering": "no",
1637
+ }
1638
+ )
1639
+
1640
+ # ๆž„ๅปบๅ“ๅบ”ๆถˆๆฏ
1641
+ response_message = {"role": "assistant"}
1642
+ if tool_calls:
1643
+ response_message["content"] = final_content if final_content else None
1644
+ response_message["tool_calls"] = tool_calls
1645
+ finish_reason = "tool_calls"
1646
+ else:
1647
+ response_message["content"] = final_content
1648
+ finish_reason = "stop"
1649
+
1650
+ response_data = ChatCompletionResponse(
1651
+ id=completion_id,
1652
+ created=created_time,
1653
+ model=request.model,
1654
+ choices=[ChatCompletionChoice(index=0, message=response_message, finish_reason=finish_reason)],
1655
+ usage=Usage(prompt_tokens=response.usage.prompt_tokens, completion_tokens=response.usage.completion_tokens, total_tokens=response.usage.total_tokens)
1656
+ )
1657
+
1658
+ log_api_call(request_log, response_data.model_dump())
1659
+
1660
+ return JSONResponse(
1661
+ content=response_data.model_dump(),
1662
+ headers={
1663
+ "Cache-Control": "no-cache",
1664
+ "X-Request-Id": completion_id,
1665
+ }
1666
+ )
1667
+ except HTTPException:
1668
+ raise
1669
+ except Exception as e:
1670
+ import traceback
1671
+ error_msg = str(e)
1672
+
1673
+ # ๆฃ€ๆต‹ๆ˜ฏๅฆๆ˜ฏ token ่ฟ‡ๆœŸ้”™่ฏฏ
1674
+ is_token_error = any(keyword in error_msg.lower() for keyword in [
1675
+ 'cookie', 'expired', '่ฟ‡ๆœŸ', '401', '403', 'unauthorized',
1676
+ 'push_id', 'snlm0e', 'upload_id', '่ฎค่ฏๅคฑ่ดฅ'
1677
+ ])
1678
+
1679
+ if is_token_error:
1680
+ print(f"[WARN] ๆฃ€ๆต‹ๅˆฐ token ๅฏ่ƒฝ่ฟ‡ๆœŸ๏ผŒๅฐ่ฏ•่‡ชๅŠจๅˆทๆ–ฐ...")
1681
+ refresh_result = try_refresh_tokens(force=True)
1682
+
1683
+ if refresh_result["success"]:
1684
+ # ๅˆทๆ–ฐๆˆๅŠŸ๏ผŒ้‡็ฝฎ client ๅนถๆ็คบ็”จๆˆท้‡่ฏ•
1685
+ reset_client()
1686
+ error_msg = f"Token ๅทฒ่‡ชๅŠจๅˆทๆ–ฐ๏ผŒ่ฏท้‡่ฏ•่ฏทๆฑ‚ใ€‚ๅŽŸ้”™่ฏฏ: {error_msg}"
1687
+ else:
1688
+ error_msg = f"Token ๅˆทๆ–ฐๅคฑ่ดฅ ({refresh_result['message']})๏ผŒ่ฏทๆ‰‹ๅŠจๆ›ดๆ–ฐ Cookieใ€‚ๅŽŸ้”™่ฏฏ: {error_msg}"
1689
+
1690
+ print(f"[ERROR] Chat error: {error_msg}")
1691
+ traceback.print_exc()
1692
+ log_api_call(request_log, None, error=error_msg)
1693
+ raise HTTPException(status_code=500, detail=error_msg)
1694
+
1695
+
1696
+ @app.post("/v1/chat/completions/reset")
1697
+ async def reset_context(authorization: str = Header(None)):
1698
+ verify_api_key(authorization)
1699
+ global _client
1700
+ if _client:
1701
+ _client.reset()
1702
+ return {"status": "ok"}
1703
+
1704
+
1705
+ # ๆณจๆ„: load_config() ๅทฒๅœจ startup_event ไธญ่ฐƒ็”จ๏ผŒ่ฟ™้‡Œไฟ็•™ๆ˜ฏไธบไบ†ๅ…ผๅฎน็›ดๆŽฅๅฏผๅ…ฅๆจกๅ—็š„ๆƒ…ๅ†ต
1706
+ load_config()
1707
+
1708
+ if __name__ == "__main__":
1709
+ print(f"""
1710
+ ๏ฟฝ๏ฟฝ๏ฟฝโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
1711
+ โ•‘ Gemini OpenAI Compatible API Server โ•‘
1712
+ โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
1713
+ โ•‘ ๅŽๅฐ้…็ฝฎ: http://localhost:{PORT}/admin โ•‘
1714
+ โ•‘ API ๅœฐๅ€: http://localhost:{PORT}/v1 โ•‘
1715
+ โ•‘ API Key: {API_KEY} โ•‘
1716
+ โ•‘ Token ่‡ชๅŠจๅˆทๆ–ฐ: {"ๅผ€ๅฏ" if TOKEN_BACKGROUND_REFRESH else "ๅ…ณ้—ญ"} ({TOKEN_REFRESH_INTERVAL_MIN}-{TOKEN_REFRESH_INTERVAL_MAX}็ง’้šๆœบ) โ•‘
1717
+ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1718
+ """)
1719
+ uvicorn.run(app, host=HOST, port=PORT)