XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

Add MiniApps provider

987642a0
hlohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

2 个文件 +369 -0
Added g4f/Provider/needs_auth/MiniApps.py +368 -0
@@ -0,0 +1,368 @@
1 from __future__ import annotations
2
3 import asyncio
4 import logging
5 import uuid
6 from typing import AsyncGenerator, Optional, Any
7
8 import aiohttp
9 try:
10 import socketio
11 has_socketio = True
12 except ImportError:
13 has_socketio = False
14
15 from ...typing import AsyncResult, Messages
16 from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
17 from ...cookies import get_cookies
18
19 logger = logging.getLogger(__name__)
20
21 # ---------------------------------------------------------------------------
22 # Constants
23 # ---------------------------------------------------------------------------
24 BASE_URL = "https://api.miniapps.ai"
25 WS_URL = "wss://api.miniapps.ai"
26 APP_URL = "https://miniapps.ai"
27 COOKIE_DOMAIN = "api.miniapps.ai"
28 COOKIE_DOMAIN2 = ".api.miniapps.ai"
29
30 _DEFAULT_HEADERS = {
31 "Accept": "application/json, text/plain, */*",
32 "Accept-Language": "en-US,en;q=0.9",
33 "Origin": APP_URL,
34 "Referer": APP_URL + "/",
35 "User-Agent": (
36 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
37 "AppleWebKit/537.36 (KHTML, like Gecko) "
38 "Chrome/124.0.0.0 Safari/537.36"
39 ),
40 }
41
42
43 # ---------------------------------------------------------------------------
44 # Provider class
45 # ---------------------------------------------------------------------------
46
47 class MiniApps(AsyncGeneratorProvider, ProviderModelMixin):
48 url = APP_URL
49 working = has_socketio # Requires socketio for streaming
50 supports_stream = True
51
52 default_model = "claude-37"
53
54 @classmethod
55 async def create_async_generator(
56 cls,
57 model: str,
58 messages: Messages,
59 proxy: str = None,
60 cookies: Optional[dict] = None,
61 **kwargs
62 ) -> AsyncResult:
63 """
64 Async generator that yields tokens from MiniApps.ai streaming chat.
65
66 Expects either:
67 - a valid session cookie stored in a file (cookie_file=...)
68 - or a Google ID token (google_id_token=...) for login
69 - or pre-authenticated cookies from environment (MINIAPPS_COOKIES)
70 """
71 if not model:
72 model = cls.default_model
73 if not cookies:
74 cookies = {** get_cookies(COOKIE_DOMAIN), ** get_cookies(COOKIE_DOMAIN2)}
75 print(f"Using cookies: {cookies}")
76 async with aiohttp.ClientSession(headers=_DEFAULT_HEADERS, cookies=cookies) as session:
77 # ---------- Step 1: CSRF token ----------
78 csrf_token = await cls._get_csrf_token(session)
79
80 # ---------- Step 2: Authenticate if needed ----------
81 # Try to use existing cookies - check if we need to login
82 # For simplicity, we assume the user provides either:
83 # - "google_id_token" in kwargs, or
84 # - "cookie_file" path to a cookies file, or
85 # - already has valid cookies from environment
86 # If none, we raise.
87 ws_token = await cls._authenticate(session, csrf_token, **kwargs)
88
89 # ---------- Step 3: Get tool info ----------
90 tool = await cls._get_tool_info(session, model, csrf_token)
91 tool_id = tool["id"]
92 revision = tool.get("revision", 1)
93 model_id = tool.get("modelId", "")
94
95 # ---------- Step 4: Send message ----------
96 # For simplicity, we only send the last message; the API may support full history via conversation_id
97 conversation_id = kwargs.get("conversation_id")
98 request_id = str(uuid.uuid4())
99
100 send_result = await cls._send_message(
101 session,
102 csrf_token,
103 tool_id,
104 revision,
105 model_id,
106 messages,
107 conversation_id=conversation_id,
108 request_id=request_id,
109 )
110 actual_conversation_id = send_result.get("conversationId", conversation_id)
111 if not conversation_id:
112 # Store for continuation if needed (optional)
113 cls.last_conversation_id = actual_conversation_id
114
115 # ---------- Step 5: Stream via Socket.IO ----------
116 # Prepare cookie string for Socket.IO authentication (from session cookies)
117 cookies = session.cookie_jar.filter_cookies(BASE_URL)
118 cookie_str = "; ".join(f"{c.key}={c.value}" for c in cookies.values())
119
120 async for token in cls._stream_response(
121 ws_token,
122 actual_conversation_id,
123 request_id,
124 cookie_str=cookie_str,
125 timeout=kwargs.get("timeout", 120)
126 ):
127 yield token
128
129 # ------------------------------------------------------------------
130 # Internal helpers
131 # ------------------------------------------------------------------
132
133 @staticmethod
134 async def _get_csrf_token(session: aiohttp.ClientSession) -> str:
135 """GET /auth/csrf and return the csrf token."""
136 url = f"{BASE_URL}/auth/csrf"
137 async with session.get(url) as resp:
138 data = await resp.json()
139 token = data.get("csrfToken", "")
140 if not token:
141 raise RuntimeError("Failed to obtain CSRF token")
142 return token
143
144 @staticmethod
145 async def _authenticate(
146 session: aiohttp.ClientSession,
147 csrf_token: str,
148 **kwargs
149 ) -> str:
150 """Authenticate if needed and return the WebSocket token (w)."""
151 # First, try to use existing cookies by calling /auth/me
152 url = f"{BASE_URL}/auth/me"
153 headers = {"x-csrf-token": csrf_token}
154 async with session.get(url, headers=headers) as resp:
155 if resp.status == 200:
156 data = await resp.json()
157 if data.get("w"):
158 return data["w"]
159 # else: not fully authenticated, need login
160
161 # If we have a google_id_token, perform Google login
162 google_id_token = kwargs.get("google_id_token")
163 if google_id_token:
164 login_url = f"{BASE_URL}/auth/google/login"
165 payload = {"idToken": google_id_token}
166 headers = {"x-csrf-token": csrf_token, "Content-Type": "application/json"}
167 async with session.post(login_url, json=payload, headers=headers) as resp:
168 data = await resp.json()
169 if not resp.ok:
170 raise RuntimeError(f"Google login failed: {data}")
171 login_hash = data.get("hash")
172 if not login_hash:
173 raise RuntimeError("No login hash returned")
174
175 # Setup user (if new account) – simplified; assumes the account exists or we just need to complete
176 # For existing accounts, we might not need setup. We'll try /auth/me again.
177 # Actually, after google_login, we should have a session. Let's try /auth/me again.
178 async with session.get(url, headers={"x-csrf-token": csrf_token}) as resp2:
179 if resp2.ok:
180 data2 = await resp2.json()
181 if data2.get("w"):
182 return data2["w"]
183 # If still not, try setup_user (if we have login_hash)
184 if login_hash:
185 setup_url = f"{BASE_URL}/auth/setup/user"
186 setup_payload = {
187 "username": kwargs.get("username", "g4f_user"),
188 "password": kwargs.get("password", "TempPass123!"),
189 "hash": login_hash,
190 }
191 async with session.post(setup_url, json=setup_payload, headers=headers) as resp3:
192 if resp3.ok:
193 data3 = await resp3.json()
194 if data3.get("w"):
195 return data3["w"]
196 raise RuntimeError("Authentication failed")
197
198 # If all fails, raise
199 raise RuntimeError(
200 "Authentication required. Provide google_id_token or valid session cookies."
201 )
202
203 @staticmethod
204 async def _get_tool_info(
205 session: aiohttp.ClientSession,
206 slug: str,
207 csrf_token: str
208 ) -> dict:
209 """Fetch tool metadata by slug (model name)."""
210 url = f"{BASE_URL}/tools/s/{slug}"
211 headers = {"x-csrf-token": csrf_token}
212 params = {"lang": "en"}
213 async with session.get(url, params=params, headers=headers) as resp:
214 if resp.status != 200:
215 raise RuntimeError(f"Tool info failed for {slug}: {await resp.text()}")
216 return await resp.json()
217
218 @staticmethod
219 async def _send_message(
220 session: aiohttp.ClientSession,
221 csrf_token: str,
222 tool_id: str,
223 revision: int,
224 model_id: str,
225 messages: list,
226 conversation_id: Optional[str] = None,
227 request_id: Optional[str] = None,
228 language: str = "en"
229 ) -> dict:
230 """POST /chat to send a message and get a conversation."""
231 if not request_id:
232 request_id = str(uuid.uuid4())
233 elements = []
234 for m in messages:
235 if m.get("role") != "user":
236 elements = []
237 if isinstance(m.get("content"), str):
238 elements.append({
239 "type": "text",
240 "text": m["content"]
241 })
242 else:
243 for part in m.get("content", []):
244 elements.append(part)
245 body = {
246 "toolId": tool_id,
247 "revision": revision,
248 "modelId": model_id,
249 "requestId": request_id,
250 "elements": elements,
251 "language": language,
252 }
253 if conversation_id:
254 body["conversationId"] = conversation_id
255
256 url = f"{BASE_URL}/chat"
257 headers = {
258 "x-csrf-token": csrf_token,
259 "Content-Type": "application/json",
260 }
261 async with session.post(url, json=body, headers=headers) as resp:
262 if not resp.ok:
263 raise RuntimeError(f"Send message failed: {await resp.text()}")
264 data = await resp.json()
265 return data
266
267 @staticmethod
268 async def _stream_response(
269 ws_token: str,
270 conversation_id: str,
271 request_id: str,
272 cookie_str: str = "",
273 timeout: int = 120
274 ) -> AsyncGenerator[str, None]:
275 """
276 Connect to Socket.IO and yield tokens from the AI stream.
277
278 Uses an asyncio.Queue to transfer data from event callbacks to
279 the generator.
280 """
281 queue: asyncio.Queue[str] = asyncio.Queue()
282 done = asyncio.Event()
283 error: Optional[Exception] = None
284
285 sio = socketio.AsyncClient(
286 logger=False,
287 engineio_logger=False,
288 reconnection=False
289 )
290
291 @sio.on("chat-token")
292 async def on_token(data):
293 print("Received token event:", data)
294 token = ""
295 if isinstance(data, str):
296 token = data
297 elif isinstance(data, dict):
298 token = data.get("text") or data.get("token") or data.get("content", "")
299 if token:
300 await queue.put(token)
301
302 @sio.on("chat-message")
303 async def on_message(data):
304 # Complete message – signal end of stream
305 done.set()
306
307 @sio.on("chat-error")
308 async def on_error(data):
309 nonlocal error
310 msg = data if isinstance(data, str) else data.get("message", str(data))
311 error = Exception(f"Stream error: {msg}")
312 done.set()
313
314 @sio.on("chat-done:{conversation_id}")
315 async def on_chat_done(data):
316 done.set()
317
318 # Connect
319 headers = {
320 "Origin": APP_URL,
321 "Cookie": cookie_str,
322 }
323 try:
324 await sio.connect(
325 WS_URL,
326 socketio_path="/socket.io/",
327 transports=["websocket"],
328 auth={"token": ws_token},
329 headers=headers,
330 wait_timeout=10
331 )
332 except Exception as e:
333 logger.error("Socket.IO connection failed: %s", e)
334 yield f"\n[Connection error: {e}]"
335 return
336
337 # We also need to register the conversation-specific done event
338 # after we know the conversation_id
339 await sio.emit("join", {"conversationId": conversation_id})
340
341 try:
342 # Loop until done or timeout
343 while True:
344 # Wait for either a token or the done event
345 token_task = asyncio.create_task(queue.get())
346 done_task = asyncio.create_task(
347 asyncio.wait_for(done.wait(), timeout=timeout)
348 )
349 # Cancel the other task when one finishes
350 token_task.add_done_callback(lambda _: done_task.cancel())
351 done_task.add_done_callback(lambda _: token_task.cancel())
352
353 try:
354 token = await token_task
355 yield token
356 except asyncio.CancelledError:
357 # Either done event fired or timeout
358 break
359 except asyncio.TimeoutError:
360 # Fallback: try to fetch conversation via REST
361 # This is optional; can yield a fallback message
362 yield f"\n[Stream timed out after {timeout}s]"
363 except Exception as exc:
364 yield f"\n[Stream error: {exc}]"
365 finally:
366 await sio.disconnect()
367 if error:
368 raise error # Re-raise the error after disconnecting
Modified g4f/Provider/needs_auth/__init__.py +1 -0
@@ -28,6 +28,7 @@ from .LMArena import LMArena
28 28 from .MetaAI import MetaAI
29 29 from .MetaAIAccount import MetaAIAccount
30 30 from .MicrosoftDesigner import MicrosoftDesigner
31 from .MiniApps import MiniApps
31 32 from .Nvidia import Nvidia
32 33 from .OpenaiAccount import OpenaiAccount
33 34 from .OpenaiAPI import OpenaiAPI