返回提交历史
Added
g4f/Provider/needs_auth/MiniApps.py
+368
-0
Modified
g4f/Provider/needs_auth/__init__.py
+1
-0
XFEstudio/gpt4free
Add MiniApps provider
987642a0
代码差异
2 个文件
+369
-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
@@ -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