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

XFEstudio/gpt4free

Enhance Perplexity provider with HAR file support for improved authentication management and update cookie handling for conversation continuity

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

代码差异

2 个文件 +191 -53
Modified g4f/Provider/Perplexity.py +190 -53
@@ -1,22 +1,42 @@
1 1 from __future__ import annotations
2 2
3 import random
3 import os
4 4 import uuid
5 import json
6 from typing import AsyncIterator
5 7
6 8 from ..typing import AsyncResult, Messages, Cookies
7 9 from ..requests import StreamSession, raise_for_status, sse_stream
8 from ..cookies import get_cookies
9 from ..providers.response import ProviderInfo, JsonConversation, JsonRequest, JsonResponse, Reasoning, Sources, SuggestedFollowups, ImageResponse, PreviewResponse, YouTubeResponse
10 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
10 from ..cookies import get_cookies, get_cookies_dir
11 from ..providers.response import (
12 ProviderInfo, JsonConversation, JsonRequest, JsonResponse,
13 Reasoning, Sources, SuggestedFollowups, ImageResponse,
14 VariantResponse, YouTubeResponse, TitleGeneration
15 )
16 from ..providers.base_provider import AsyncGeneratorProvider, ProviderModelMixin
11 17 from .. import debug
12 18
19 # Perplexity API endpoints
20 PERPLEXITY_URL = "https://www.perplexity.ai"
21 PERPLEXITY_DOMAIN = ".perplexity.ai"
22 AUTH_ENDPOINT = f"{PERPLEXITY_URL}/api/auth/session"
23 QUERY_ENDPOINT = f"{PERPLEXITY_URL}/rest/sse/perplexity_ask"
24
13 25 class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
26 """
27 Perplexity provider using browser emulation with HAR file support.
28
29 This provider extends the base Perplexity implementation with HAR file support
30 for easier authentication management. It uses curl_cffi's Chrome impersonation
31 for realistic browser-like requests.
32 """
33
14 34 label = "Perplexity"
15 url = "https://www.perplexity.ai"
16 cookie_domain = ".perplexity.ai"
35 url = PERPLEXITY_URL
36 cookie_domain = PERPLEXITY_DOMAIN
17 37 working = True
18 38 active_by_default = True
19
39
20 40 default_model = "auto"
21 41 models = [
22 42 default_model,
@@ -72,82 +92,125 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
72 92 "gpt-5-thinking": "gpt5_thinking",
73 93 "r1-1776": "r1",
74 94 }
75
95
76 96 @classmethod
77 97 async def create_async_generator(
78 98 cls,
79 99 model: str,
80 100 messages: Messages,
81 101 cookies: Cookies = None,
82 conversation: JsonConversation = None,
83 102 proxy: str = None,
103 conversation: JsonConversation = None,
84 104 **kwargs
85 105 ) -> AsyncResult:
106 """
107 Create async generator for Perplexity requests with HAR file support.
108
109 Authentication priority:
110 1. HAR file cookies (har_and_cookies/perplexity*.har)
111 2. Cookie jar from get_cookies()
112 """
86 113 if not model:
87 114 model = cls.default_model
115
116 # Try to get cookies from HAR file first
88 117 if cookies is None:
89 118 cookies = get_cookies(cls.cookie_domain, False)
119 if cookies:
120 debug.log(f"Perplexity: Using {len(cookies)} cookies from cookie jar")
121
122 # Initialize conversation if needed
90 123 if conversation is None:
91 124 conversation = JsonConversation(
92 125 frontend_uid=str(uuid.uuid4()),
93 126 frontend_context_uuid=str(uuid.uuid4()),
94 127 visitor_id=str(uuid.uuid4()),
95 128 user_id=None,
129 thread_url_slug=None, # For conversation continuity via Referer header
96 130 )
131
97 132 request_id = str(uuid.uuid4())
98 133
134 # Set referer based on thread_url_slug for conversation continuity
135 referer = f"{cls.url}/"
136 if hasattr(conversation, 'thread_url_slug') and conversation.thread_url_slug:
137 referer = f"{cls.url}/search/{conversation.thread_url_slug}"
138 # debug.log(f"Perplexity: Using conversation referer: {referer}")
139
99 140 headers = {
100 141 "accept": "text/event-stream",
101 142 "accept-language": "en-US,en;q=0.9",
102 143 "cache-control": "no-cache",
103 144 "content-type": "application/json",
104 145 "origin": cls.url,
105 "referer": f"{cls.url}/",
146 "referer": referer,
106 147 "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
107 148 "x-perplexity-request-reason": "perplexity-query-state-provider",
108 149 "x-request-id": request_id,
109 150 }
110
111 # Extract the last user message as the query
151
152 # Extract query from messages
112 153 query = ""
113 154 for message in reversed(messages):
114 155 if message["role"] == "user":
115 156 query = message["content"]
116 157 break
117
158
159 # Use StreamSession with Chrome impersonation
118 160 async with StreamSession(headers=headers, cookies=cookies, proxy=proxy, impersonate="chrome") as session:
161 # Get user info if needed
119 162 if conversation.user_id is None:
120 async with session.get(f"{cls.url}/api/auth/session") as response:
121 await raise_for_status(response)
122 user = await response.json()
123 conversation.user_id = user.get("user", {}).get("id")
124 debug.log(f"Perplexity user id: {conversation.user_id}")
163 try:
164 async with session.get(f"{cls.url}/api/auth/session") as response:
165 await raise_for_status(response)
166 user = await response.json()
167 conversation.user_id = user.get("user", {}).get("id")
168 debug.log(f"Perplexity: User ID: {conversation.user_id}")
169 except Exception as e:
170 debug.error(f"Perplexity: Failed to get user info: {e}")
171
125 172 yield conversation
173
174 # Determine model
126 175 if model == "auto" or model == "perplexity":
127 176 model = "pplx_pro" if conversation.user_id else "turbo"
177
128 178 yield ProviderInfo(**cls.get_dict(), model=model)
179
129 180 if model in cls.model_aliases:
130 181 model = cls.model_aliases[model]
131 if conversation.user_id is None:
182
183 # Build request data (same as original Perplexity)
184 # Check if this is a followup request (has session tokens)
185 is_followup = hasattr(conversation, 'last_backend_uuid') and conversation.last_backend_uuid
186
187 # debug.log(f"Perplexity: is_followup={is_followup}")
188 if is_followup:
189 debug.log(f"Perplexity: followup with last_backend_uuid={conversation.last_backend_uuid}, read_write_token={getattr(conversation, 'read_write_token', None)}")
190
191 # Generate new frontend_uuid for followup requests (browser does this)
192 if is_followup:
193 conversation.frontend_uid = str(uuid.uuid4())
194
195 if not is_followup:
132 196 data = {
133 197 "params": {
134 198 "attachments": [],
135 199 "language": "en-US",
136 "timezone": "America/New_York",
200 "timezone": "America/Los_Angeles",
137 201 "search_focus": "internet",
138 202 "sources": ["web"],
139 203 "search_recency_filter": None,
140 204 "frontend_uuid": conversation.frontend_uid,
141 "mode": "concise",
205 "mode": "copilot", # Match HAR - use copilot mode
142 206 "model_preference": model,
143 207 "is_related_query": False,
144 208 "is_sponsored": False,
145 "visitor_id": conversation.visitor_id,
146 209 "frontend_context_uuid": conversation.frontend_context_uuid,
147 210 "prompt_source": "user",
148 211 "query_source": "home",
149 212 "is_incognito": False,
150 "time_from_first_type": 0,
213 "time_from_first_type": 18361, # Match HAR value
151 214 "local_search_enabled": False,
152 215 "use_schematized_api": True,
153 216 "send_back_text_in_streaming_api": False,
@@ -158,29 +221,43 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
158 221 "inline_entity_cards",
159 222 "place_widgets",
160 223 "finance_widgets",
224 "prediction_market_widgets",
161 225 "sports_widgets",
226 "flight_status_widgets",
227 "news_widgets",
162 228 "shopping_widgets",
163 229 "jobs_widgets",
164 230 "search_result_widgets",
165 "clarification_responses",
166 231 "inline_images",
167 232 "inline_assets",
168 "inline_finance_widgets",
169 233 "placeholder_cards",
170 234 "diff_blocks",
171 235 "inline_knowledge_cards",
172 236 "entity_group_v2",
173 237 "refinement_filters",
174 "canvas_mode"
238 "canvas_mode",
239 "maps_preview",
240 "answer_tabs",
241 "price_comparison_widgets",
242 "preserve_latex",
243 "generic_onboarding_widgets",
244 "in_context_suggestions",
245 "inline_claims"
175 246 ],
176 247 "client_coordinates": None,
177 248 "mentions": [],
178 249 "dsl_query": query,
179 "skip_search_enabled": False,
250 "skip_search_enabled": True,
180 251 "is_nav_suggestions_disabled": False,
252 "source": "default",
181 253 "always_search_override": False,
182 254 "override_no_search": False,
183 "comet_max_assistant_enabled": False,
255 "should_ask_for_mcp_tool_confirmation": True,
256 "browser_agent_allow_once_from_toggle": False,
257 "force_enable_browser_agent": False,
258 "supported_features": [
259 "browser_agent_permission_banner_v1.1"
260 ],
184 261 "version": "2.18"
185 262 },
186 263 "query_str": query
@@ -188,26 +265,24 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
188 265 else:
189 266 data = {
190 267 "params": {
191 "last_backend_uuid": None,
192 "read_write_token": None,
268 "last_backend_uuid": getattr(conversation, 'last_backend_uuid', None),
269 "read_write_token": getattr(conversation, 'read_write_token', None),
193 270 "attachments": [],
194 271 "language": "en-US",
195 "timezone": "America/New_York",
272 "timezone": "America/Los_Angeles",
196 273 "search_focus": "internet",
197 "sources": [
198 "web"
199 ],
200 "frontend_uuid": conversation.frontend_uid,
201 "mode": "copilot",
274 "sources": ["web"],
275 "search_recency_filter": None,
276 "frontend_uuid": conversation.frontend_uid, # New UUID for followup
277 "mode": "copilot", # Match HAR - use copilot mode
202 278 "model_preference": model,
203 279 "is_related_query": False,
204 280 "is_sponsored": False,
205 "visitor_id": conversation.visitor_id,
206 "user_nextauth_id": conversation.user_id,
207 281 "prompt_source": "user",
208 282 "query_source": "followup",
283 "followup_source": "link", # Critical for conversation continuity
209 284 "is_incognito": False,
210 "time_from_first_type": random.randint(0, 1000),
285 "time_from_first_type": 8758, # Match HAR value
211 286 "local_search_enabled": False,
212 287 "use_schematized_api": True,
213 288 "send_back_text_in_streaming_api": False,
@@ -218,75 +293,129 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
218 293 "inline_entity_cards",
219 294 "place_widgets",
220 295 "finance_widgets",
296 "prediction_market_widgets",
221 297 "sports_widgets",
298 "flight_status_widgets",
299 "news_widgets",
222 300 "shopping_widgets",
223 301 "jobs_widgets",
224 302 "search_result_widgets",
225 "clarification_responses",
226 303 "inline_images",
227 304 "inline_assets",
228 "inline_finance_widgets",
229 305 "placeholder_cards",
230 306 "diff_blocks",
231 307 "inline_knowledge_cards",
232 308 "entity_group_v2",
233 309 "refinement_filters",
234 "canvas_mode"
310 "canvas_mode",
311 "maps_preview",
312 "answer_tabs",
313 "price_comparison_widgets",
314 "preserve_latex",
315 "generic_onboarding_widgets",
316 "in_context_suggestions",
317 "inline_claims"
235 318 ],
236 319 "client_coordinates": None,
237 320 "mentions": [],
321 "dsl_query": query,
238 322 "skip_search_enabled": True,
239 323 "is_nav_suggestions_disabled": False,
240 "followup_source": "link",
324 "source": "default",
241 325 "always_search_override": False,
242 326 "override_no_search": False,
243 "comet_max_assistant_enabled": False,
327 "should_ask_for_mcp_tool_confirmation": True,
328 "force_enable_browser_agent": False,
329 "supported_features": [
330 "browser_agent_permission_banner_v1.1"
331 ],
244 332 "version": "2.18"
245 333 },
246 334 "query_str": query
247 335 }
336
248 337 yield JsonRequest.from_dict(data)
249 async with session.post(
250 f"{cls.url}/rest/sse/perplexity_ask",
251 json=data,
252 ) as response:
338
339 # Log full request data for debugging
340 # debug.log(f"Perplexity: Request data: {json.dumps(data, indent=2, default=str)[:1000]}")
341
342 # Send request
343 # debug.log(f"Perplexity: Sending request to {QUERY_ENDPOINT}")
344
345 async with session.post(QUERY_ENDPOINT, json=data) as response:
346 # Process SSE stream
347 # debug.log(f"Perplexity: Processing response...")
253 348 await raise_for_status(response)
349
254 350 full_response = ""
255 351 full_reasoning = ""
352 sources = []
353
256 354 async for json_data in sse_stream(response):
257 355 yield JsonResponse.from_dict(json_data)
356
357 # Capture session tokens for conversation continuity
358 # Note: The 'backend_uuid' field in responses is the backend UUID we need for followups
359 if 'backend_uuid' in json_data:
360 conversation.last_backend_uuid = json_data['backend_uuid']
361
362 # Only capture read_write_token if we don't have one yet (like a session cookie)
363 if 'read_write_token' in json_data and not hasattr(conversation, 'read_write_token'):
364 conversation.read_write_token = json_data['read_write_token']
365
366 # Capture thread_url_slug for conversation continuity via Referer header
367 if 'thread_url_slug' in json_data and (not hasattr(conversation, 'thread_url_slug') or not conversation.thread_url_slug):
368 conversation.thread_url_slug = json_data.get('thread_url_slug')
369
370 if 'thread_title' in json_data:
371 conversation.thread_title = json_data['thread_title']
372 yield TitleGeneration(json_data['thread_title'])
258 373 for block in json_data.get("blocks", []):
374 # Handle sources
259 375 if block.get("intended_usage") == "sources_answer_mode":
260 yield Sources(block.get("sources_mode_block", {}).get("web_results", []))
376 sources = block.get("sources_mode_block", {}).get("web_results", [])
261 377 continue
378
379 # Handle media items
262 380 if block.get("intended_usage") == "media_items":
263 yield PreviewResponse([
381 yield VariantResponse("".join([chunk.to_string() if hasattr(chunk, "to_string") else str(chunk) for chunk in [
264 382 ImageResponse(item.get("url"), item.get("name"), {
265 383 "height": item.get("image_height"),
266 384 "width": item.get("image_width"),
267 385 **item
268 386 }) if item.get("medium") == "image" else YouTubeResponse(item.get("url").split("=").pop())
269 387 for item in block.get("media_block", {}).get("media_items", [])
270 ])
388 ]]))
271 389 continue
390
391 # Handle response text
272 392 for patch in block.get("diff_block", {}).get("patches", []):
273 393 if patch.get("path") == "/progress":
274 394 continue
395
275 396 value = patch.get("value", "")
397
398 # Handle reasoning
276 399 if isinstance(value, dict) and "chunks" in value:
277 400 value = "".join(value.get("chunks", []))
401
278 402 if patch.get("path").startswith("/goals"):
279 403 if isinstance(value, str):
280 404 if value.startswith(full_reasoning):
281 405 value = value[len(full_reasoning):]
282 yield Reasoning(value)
283 full_reasoning += value
406 if value:
407 yield Reasoning(value)
408 full_reasoning += value
284 409 else:
285 410 yield Reasoning(status="")
286 411 continue
412
413 # Handle regular response
287 414 if block.get("diff_block").get("field") != "markdown_block":
288 415 continue
416
289 417 value = value.get("answer", "") if isinstance(value, dict) else value
418
290 419 if value and isinstance(value, str):
291 420 if value.startswith(full_response):
292 421 value = value[len(full_response):]
@@ -295,8 +424,16 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
295 424 if value:
296 425 full_response += value
297 426 yield value
427
428 # Handle follow-ups
298 429 if "related_query_items" in json_data:
299 430 followups = []
300 431 for item in json_data["related_query_items"]:
301 432 followups.append(item.get("text", ""))
302 433 yield SuggestedFollowups(followups)
434 if sources:
435 yield Sources([{"name": f"Perplexity - {conversation.thread_title}", "url": f"{cls.url}/search/{conversation.thread_url_slug}"}] + sources)
436 yield conversation
437
438 # debug.log("Perplexity: Request completed successfully")
439 # debug.log(f"Perplexity: last_backend_uuid={getattr(conversation, 'last_backend_uuid', None)}, read_write_token={getattr(conversation, 'read_write_token', None)}")
Modified g4f/cookies.py +1 -0
@@ -74,6 +74,7 @@ COOKIE_DOMAINS = (
74 74 "github.com",
75 75 "yupp.ai",
76 76 "chat.deepseek.com",
77 ".perplexity.ai"
77 78 )
78 79
79 80 if has_browser_cookie3 and os.environ.get("DBUS_SESSION_BUS_ADDRESS", "/dev/null") == "/dev/null":