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

XFEstudio/gpt4free

feat: introduce render_messages and enhance HAR/conversation handling

- **g4f/providers/helper.py** - Add `render_messages()` to normalise message contents that are lists of blocks. - **g4f/Provider/Blackbox.py** - Import `get_har_files` and `render_messages`. - Replace manual walk of `get_cookies_dir()` with `get_har_files()` in `_find_session_in_har`. - Simplify session‑parsing loop and exception logging; drop permissions check. - Build `current_messages` with `render_messages(messages)` instead of raw list. - **g4f/Provider/Cloudflare.py** - Swap `to_string` import for `render_messages`. - Add `"impersonate": "chrome"` to default `_args`. - Construct `data["messages"]` with `render_messages(messages)` and inline `"parts"`; remove `to_string()` calls. - Move `cache_file` write outside inner `try` to always save arguments. - **g4f/Provider/Copilot.py** - Defer `yield conversation` until after `conversation` is created when `return_conversation` is requested. - **g4f/Provider/openai/har_file.py** - Break out of `os.walk` after first directory in `get_har_files()` to avoid deep traversal. - **g4f/api/__init__.py** - Use `config.conversation` directly and set `return_conversation` when present. - **g4f/client/__init__.py** - Pass `conversation` to both `ChatCompletionChunk.model_construct()` and `ChatCompletion.model_construct()`. - **g4f/client/stubs.py** - Import `field_serializer` (with stub fallback). - Add serializers for `conversation` (objects and dicts) and for `content` fields. - Extend model constructors to accept/propagate `conversation`. - **g4f/cookies.py** - Insert ".huggingface.co" into `DOMAINS` list. - Stop recursive directory walk in `read_cookie_files()` with early `break`. - **g4f/gui/client/background.html** - Reorder error‑handling branches; reset `errorImage` in `onload`. - Revise `skipRefresh` logic and random image URL building. - **g4f/gui/server/backend_api.py** - Add `self.match_files` cache for repeated image searches. - Use `safe_search` for sanitised term matching and `min` comparison. - Limit walk to one directory level; support deterministic random selection via `random` query param. - **Miscellaneous** - Update imports where `render_messages` replaces `to_string`. - Ensure all modified providers iterate messages through `render_messages` for consistent formatting.

3ab36ebc
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

11 个文件 +125 -86
Modified g4f/Provider/Blackbox.py +43 -49
@@ -14,9 +14,10 @@ from datetime import datetime, timedelta
14 14 from ..typing import AsyncResult, Messages, MediaListType
15 15 from ..requests.raise_for_status import raise_for_status
16 16 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
17 from .openai.har_file import get_har_files
17 18 from ..image import to_data_uri
18 19 from ..cookies import get_cookies_dir
19 from .helper import format_image_prompt
20 from .helper import format_image_prompt, render_messages
20 21 from ..providers.response import JsonConversation, ImageResponse
21 22 from ..tools.media import merge_media
22 23 from ..errors import RateLimitError
@@ -428,53 +429,46 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
428 429 Optional[dict]: Session data if found, None otherwise
429 430 """
430 431 try:
431 har_dir = get_cookies_dir()
432 if not os.access(har_dir, os.R_OK):
433 return None
434
435 for root, _, files in os.walk(har_dir):
436 for file in files:
437 if file.endswith(".har"):
438 try:
439 with open(os.path.join(root, file), 'rb') as f:
440 har_data = json.load(f)
441
442 for entry in har_data['log']['entries']:
443 # Only look at blackbox API responses
444 if 'blackbox.ai/api' in entry['request']['url']:
445 # Look for a response that has the right structure
446 if 'response' in entry and 'content' in entry['response']:
447 content = entry['response']['content']
448 # Look for both regular and Google auth session formats
449 if ('text' in content and
450 isinstance(content['text'], str) and
451 '"user"' in content['text'] and
452 '"email"' in content['text'] and
453 '"expires"' in content['text']):
454
455 try:
456 # Remove any HTML or other non-JSON content
457 text = content['text'].strip()
458 if text.startswith('{') and text.endswith('}'):
459 # Replace escaped quotes
460 text = text.replace('\\"', '"')
461 har_session = json.loads(text)
462
463 # Check if this is a valid session object
464 if (isinstance(har_session, dict) and
465 'user' in har_session and
466 'email' in har_session['user'] and
467 'expires' in har_session):
468
469 debug.log(f"Blackbox: Found session in HAR file: {file}")
470 return har_session
471 except json.JSONDecodeError as e:
472 # Only print error for entries that truly look like session data
473 if ('"user"' in content['text'] and
474 '"email"' in content['text']):
475 debug.log(f"Blackbox: Error parsing likely session data: {e}")
476 except Exception as e:
477 debug.log(f"Blackbox: Error reading HAR file {file}: {e}")
432 for file in get_har_files():
433 try:
434 with open(file, 'rb') as f:
435 har_data = json.load(f)
436
437 for entry in har_data['log']['entries']:
438 # Only look at blackbox API responses
439 if 'blackbox.ai/api' in entry['request']['url']:
440 # Look for a response that has the right structure
441 if 'response' in entry and 'content' in entry['response']:
442 content = entry['response']['content']
443 # Look for both regular and Google auth session formats
444 if ('text' in content and
445 isinstance(content['text'], str) and
446 '"user"' in content['text'] and
447 '"email"' in content['text'] and
448 '"expires"' in content['text']):
449 try:
450 # Remove any HTML or other non-JSON content
451 text = content['text'].strip()
452 if text.startswith('{') and text.endswith('}'):
453 # Replace escaped quotes
454 text = text.replace('\\"', '"')
455 har_session = json.loads(text)
456
457 # Check if this is a valid session object
458 if (isinstance(har_session, dict) and
459 'user' in har_session and
460 'email' in har_session['user'] and
461 'expires' in har_session):
462
463 debug.log(f"Blackbox: Found session in HAR file: {file}")
464 return har_session
465 except json.JSONDecodeError as e:
466 # Only print error for entries that truly look like session data
467 if ('"user"' in content['text'] and
468 '"email"' in content['text']):
469 debug.log(f"Blackbox: Error parsing likely session data: {e}")
470 except Exception as e:
471 debug.log(f"Blackbox: Error reading HAR file {file}: {e}")
478 472 return None
479 473 except Exception as e:
480 474 debug.log(f"Blackbox: Error searching HAR files: {e}")
@@ -573,7 +567,7 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
573 567 conversation.message_history = []
574 568
575 569 current_messages = []
576 for i, msg in enumerate(messages):
570 for i, msg in enumerate(render_messages(messages)):
577 571 msg_id = conversation.chat_id if i == 0 and msg["role"] == "user" else cls.generate_id()
578 572 current_msg = {
579 573 "id": msg_id,
Modified g4f/Provider/Cloudflare.py +5 -6
@@ -9,7 +9,7 @@ from ..requests import Session, StreamSession, get_args_from_nodriver, raise_for
9 9 from ..requests import DEFAULT_HEADERS, has_nodriver, has_curl_cffi
10 10 from ..providers.response import FinishReason, Usage
11 11 from ..errors import ResponseStatusError, ModelNotFoundError
12 from .helper import to_string
12 from .helper import render_messages
13 13
14 14 class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
15 15 label = "Cloudflare AI"
@@ -82,7 +82,7 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
82 82 elif has_nodriver:
83 83 cls._args = await get_args_from_nodriver(cls.url, proxy, timeout, cookies)
84 84 else:
85 cls._args = {"headers": DEFAULT_HEADERS, "cookies": {}}
85 cls._args = {"headers": DEFAULT_HEADERS, "cookies": {}, "impersonate": "chrome"}
86 86 try:
87 87 model = cls.get_model(model)
88 88 except ModelNotFoundError:
@@ -90,8 +90,7 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
90 90 data = {
91 91 "messages": [{
92 92 **message,
93 "content": to_string(message["content"]),
94 "parts": [{"type":"text", "text": to_string(message["content"])}]} for message in messages],
93 "parts": [{"type":"text", "text": message["content"]}]} for message in render_messages(messages)],
95 94 "lora": None,
96 95 "model": model,
97 96 "max_tokens": max_tokens,
@@ -120,5 +119,5 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
120 119 yield Usage(**finish.get("usage"))
121 120 yield FinishReason(finish.get("finishReason"))
122 121
123 with cache_file.open("w") as f:
124 json.dump(cls._args, f)
122 with cache_file.open("w") as f:
123 json.dump(cls._args, f)
Modified g4f/Provider/Copilot.py +2 -2
@@ -116,8 +116,6 @@ class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
116 116 response.raise_for_status()
117 117 conversation_id = response.json().get("id")
118 118 conversation = Conversation(conversation_id)
119 if return_conversation:
120 yield conversation
121 119 if prompt is None:
122 120 prompt = format_prompt_max_length(messages, 10000)
123 121 debug.log(f"Copilot: Created conversation: {conversation_id}")
@@ -126,6 +124,8 @@ class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
126 124 if prompt is None:
127 125 prompt = get_last_user_message(messages)
128 126 debug.log(f"Copilot: Use conversation: {conversation_id}")
127 if return_conversation:
128 yield conversation
129 129
130 130 uploaded_images = []
131 131 for media, _ in merge_media(media, messages):
Modified g4f/Provider/openai/har_file.py +1 -0
@@ -49,6 +49,7 @@ def get_har_files():
49 49 for file in files:
50 50 if file.endswith(".har"):
51 51 harPath.append(os.path.join(root, file))
52 break
52 53 if not harPath:
53 54 raise NoValidHarFileError("No .har file found")
54 55 harPath.sort(key=lambda x: os.path.getmtime(x))
Modified g4f/api/__init__.py +2 -2
@@ -309,9 +309,9 @@ class Api:
309 309 if credentials is not None and credentials.credentials != "secret":
310 310 config.api_key = credentials.credentials
311 311
312 conversation = None
312 conversation = config.conversation
313 313 return_conversation = config.return_conversation
314 if conversation is not None:
314 if conversation:
315 315 conversation = JsonConversation(**conversation)
316 316 return_conversation = True
317 317 elif config.conversation_id is not None and config.provider is not None:
Modified g4f/client/__init__.py +2 -2
@@ -217,7 +217,7 @@ async def async_iter_response(
217 217
218 218 if stream:
219 219 chat_completion = ChatCompletionChunk.model_construct(
220 None, finish_reason, completion_id, int(time.time()), usage=usage
220 None, finish_reason, completion_id, int(time.time()), usage=usage, conversation=conversation
221 221 )
222 222 else:
223 223 if response_format is not None and "type" in response_format:
@@ -228,7 +228,7 @@ async def async_iter_response(
228 228 **filter_none(
229 229 tool_calls=[ToolCallModel.model_construct(**tool_call) for tool_call in tool_calls]
230 230 ) if tool_calls is not None else {},
231 conversation=None if conversation is None else conversation.get_dict()
231 conversation=conversation
232 232 )
233 233 if provider is not None:
234 234 chat_completion.provider = provider.name
Modified g4f/client/stubs.py +28 -3
@@ -10,7 +10,7 @@ from ..client.helper import filter_markdown
10 10 from .helper import filter_none
11 11
12 12 try:
13 from pydantic import BaseModel
13 from pydantic import BaseModel, field_serializer
14 14 except ImportError:
15 15 class BaseModel():
16 16 @classmethod
@@ -19,6 +19,9 @@ except ImportError:
19 19 for key, value in data.items():
20 20 setattr(new, key, value)
21 21 return new
22 class field_serializer():
23 def __init__(self, field_name):
24 self.field_name = field_name
22 25
23 26 class BaseModel(BaseModel):
24 27 @classmethod
@@ -72,6 +75,7 @@ class ChatCompletionChunk(BaseModel):
72 75 provider: Optional[str]
73 76 choices: List[ChatCompletionDeltaChoice]
74 77 usage: UsageModel
78 conversation: dict
75 79
76 80 @classmethod
77 81 def model_construct(
@@ -80,7 +84,8 @@ class ChatCompletionChunk(BaseModel):
80 84 finish_reason: str,
81 85 completion_id: str = None,
82 86 created: int = None,
83 usage: UsageModel = None
87 usage: UsageModel = None,
88 conversation: dict = None
84 89 ):
85 90 return super().model_construct(
86 91 id=f"chatcmpl-{completion_id}" if completion_id else None,
@@ -92,9 +97,15 @@ class ChatCompletionChunk(BaseModel):
92 97 ChatCompletionDelta.model_construct(content),
93 98 finish_reason
94 99 )],
95 **filter_none(usage=usage)
100 **filter_none(usage=usage, conversation=conversation)
96 101 )
97 102
103 @field_serializer('conversation')
104 def serialize_conversation(self, conversation: dict):
105 if hasattr(conversation, "get_dict"):
106 return conversation.get_dict()
107 return conversation
108
98 109 class ChatCompletionMessage(BaseModel):
99 110 role: str
100 111 content: str
@@ -104,6 +115,10 @@ class ChatCompletionMessage(BaseModel):
104 115 def model_construct(cls, content: str, tool_calls: list = None):
105 116 return super().model_construct(role="assistant", content=content, **filter_none(tool_calls=tool_calls))
106 117
118 @field_serializer('content')
119 def serialize_content(self, content: str):
120 return str(content)
121
107 122 def save(self, filepath: str, allowd_types = None):
108 123 if hasattr(self.content, "data"):
109 124 os.rename(self.content.data.replace("/media", images_dir), filepath)
@@ -160,6 +175,12 @@ class ChatCompletion(BaseModel):
160 175 **filter_none(usage=usage, conversation=conversation)
161 176 )
162 177
178 @field_serializer('conversation')
179 def serialize_conversation(self, conversation: dict):
180 if hasattr(conversation, "get_dict"):
181 return conversation.get_dict()
182 return conversation
183
163 184 class ChatCompletionDelta(BaseModel):
164 185 role: str
165 186 content: str
@@ -168,6 +189,10 @@ class ChatCompletionDelta(BaseModel):
168 189 def model_construct(cls, content: Optional[str]):
169 190 return super().model_construct(role="assistant", content=content)
170 191
192 @field_serializer('content')
193 def serialize_content(self, content: str):
194 return str(content)
195
171 196 class ChatCompletionDeltaChoice(BaseModel):
172 197 index: int
173 198 delta: ChatCompletionDelta
Modified g4f/cookies.py +2 -2
@@ -56,12 +56,11 @@ DOMAINS = [
56 56 ".google.com",
57 57 "www.whiterabbitneo.com",
58 58 "huggingface.co",
59 ".huggingface.co"
59 60 "chat.reka.ai",
60 61 "chatgpt.com",
61 62 ".cerebras.ai",
62 63 "github.com",
63 "huggingface.co",
64 ".huggingface.co"
65 64 ]
66 65
67 66 if has_browser_cookie3 and os.environ.get('DBUS_SESSION_BUS_ADDRESS') == "/dev/null":
@@ -152,6 +151,7 @@ def read_cookie_files(dirPath: str = None):
152 151 harFiles.append(os.path.join(root, file))
153 152 elif file.endswith(".json"):
154 153 cookieFiles.append(os.path.join(root, file))
154 break
155 155
156 156 CookiesConfig.cookies = {}
157 157 for path in harFiles:
Modified g4f/gui/client/background.html +8 -5
@@ -169,15 +169,15 @@
169 169 if (errorVideo < 3 || !refreshOnHide) {
170 170 return;
171 171 }
172 if (skipRefresh > 0) {
173 skipRefresh -= 1;
174 return;
175 }
172 176 if (errorImage < 3) {
173 177 imageFeed.src = "/search/image+g4f?skip=" + skipImage;
174 178 skipImage++;
175 179 return;
176 180 }
177 if (skipRefresh > 0) {
178 skipRefresh -= 1;
179 return;
180 }
181 181 if (images.length > 0) {
182 182 imageFeed.classList.remove("hidden");
183 183 imageFeed.src = images.shift();
@@ -194,10 +194,13 @@
194 194 imageFeed.onload = () => {
195 195 imageFeed.classList.remove("hidden");
196 196 gradient.classList.add("hidden");
197 errorImage = 0;
197 198 };
198 199 imageFeed.onclick = () => {
199 200 imageFeed.src = "/search/image?random=" + Math.random();
200 skipRefresh = 2;
201 if (skipRefresh < 4) {
202 skipRefresh += 1;
203 }
201 204 };
202 205 })();
203 206 </script>
Modified g4f/gui/server/backend_api.py +22 -15
@@ -341,28 +341,35 @@ class Backend_Api(Api):
341 341 return redirect(source_url)
342 342 raise
343 343
344 self.match_files = {}
345
344 346 @app.route('/search/<search>', methods=['GET'])
345 347 def find_media(search: str):
346 search = [secure_filename(chunk.lower()) for chunk in search.split("+")]
348 safe_search = [secure_filename(chunk.lower()) for chunk in search.split("+")]
347 349 if not os.access(images_dir, os.R_OK):
348 350 return jsonify({"error": {"message": "Not found"}}), 404
349 match_files = {}
350 for root, _, files in os.walk(images_dir):
351 for file in files:
352 mime_type = is_allowed_extension(file)
353 if mime_type is not None:
354 mime_type = secure_filename(mime_type)
355 for tag in search:
356 if tag in mime_type:
357 match_files[file] = match_files.get(file, 0) + 1
358 break
359 for tag in search:
360 if tag in file.lower():
361 match_files[file] = match_files.get(file, 0) + 1
362 match_files = [file for file, count in match_files.items() if count >= request.args.get("min", len(search))]
351 if search not in self.match_files:
352 self.match_files[search] = {}
353 for root, _, files in os.walk(images_dir):
354 for file in files:
355 mime_type = is_allowed_extension(file)
356 if mime_type is not None:
357 mime_type = secure_filename(mime_type)
358 for tag in safe_search:
359 if tag in mime_type:
360 self.match_files[search][file] = self.match_files[search].get(file, 0) + 1
361 break
362 for tag in safe_search:
363 if tag in file.lower():
364 self.match_files[search][file] = self.match_files[search].get(file, 0) + 1
365 break
366 match_files = [file for file, count in self.match_files[search].items() if count >= request.args.get("min", len(safe_search))]
363 367 if int(request.args.get("skip", 0)) >= len(match_files):
364 368 return jsonify({"error": {"message": "Not found"}}), 404
365 369 if (request.args.get("random", False)):
370 seed = request.args.get("random")
371 if seed not in ["true", "True", "1"]:
372 random.seed(seed)
366 373 return redirect(f"/media/{random.choice(match_files)}"), 302
367 374 return redirect(f"/media/{match_files[int(request.args.get('skip', 0))]}", 302)
368 375
Modified g4f/providers/helper.py +10 -0
@@ -24,6 +24,16 @@ def to_string(value) -> str:
24 24 return "".join([to_string(v) for v in value if v.get("type", "text") == "text"])
25 25 return str(value)
26 26
27 def render_messages(messages: Messages) -> Iterator:
28 for idx, message in enumerate(messages):
29 if isinstance(message, dict) and isinstance(message.get("content"), list):
30 yield {
31 **message,
32 "content": to_string(message["content"]),
33 }
34 else:
35 yield message
36
27 37 def format_prompt(messages: Messages, add_special_tokens: bool = False, do_continue: bool = False, include_system: bool = True) -> str:
28 38 """
29 39 Format a series of messages into a single string, optionally adding special tokens.