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

XFEstudio/gpt4free

Enhance Google AI Mode functionality and improve screenshot handling

- Updated GoogleAiMode to include ai-mode parameter in search URL. - Refactored JavaScript for better button click handling and added support for AI mode. - Modified API to dynamically set headless mode for CDPSession based on URL. - Added new endpoint for serving screenshots from the filesystem. - Improved HTML structure by removing unnecessary grid layout. - Updated website.py to enhance logo URL handling and input field functionality.

8604474b
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

5 个文件 +224 -106
Modified g4f/Provider/search/GoogleAiMode.py +20 -61
@@ -27,7 +27,7 @@ class GoogleAiMode(GoogleSearch):
27 27 **kwargs,
28 28 ) -> AsyncResult:
29 29 query = get_last_user_message(messages)
30 search_url = f"{cls.url}/search?q={urllib.parse.quote_plus(query)}"
30 search_url = f"{cls.url}/search?q={urllib.parse.quote_plus(query)}&ai-mode=true"
31 31
32 32 debug.log(f"Google Search: Starting CDPSession for query: {query}")
33 33 session = CDPSession(headless=False)
@@ -53,72 +53,31 @@ class GoogleAiMode(GoogleSearch):
53 53
54 54 try:
55 55 for _ in range(5):
56 result = await session.evaluate_js("""const b =Array.from(document.querySelectorAll("a, button")).filter(a=>a.textContent.endsWith("KI‑Modus") || a.textContent.endsWith("AI-Mode")).pop(); b ? b.click() : null; !!b""")
56 result = await session.evaluate_js("""const b = Array.from(document.querySelectorAll("a, button")).filter(a => {
57 return a.textContent.endsWith("KI‑Modus") || a.textContent.endsWith("AI Mode");
58 }).pop(); b ? b.click() : null; !!b""")
57 59 await asyncio.sleep(1)
58 60 if not result:
59 61 continue
60 62 await session.wait_for_network_idle(idle_time=1, timeout=10.0)
61 results = await session.call("Runtime.evaluate", expression=r"""const cyrb53 = (str, seed = 0) => {
62 let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
63 for(let i = 0, ch; i < str.length; i++) {
64 ch = str.charCodeAt(i);
65 h1 = Math.imul(h1 ^ ch, 2654435761);
66 h2 = Math.imul(h2 ^ ch, 1597334677);
67 }
68 h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
69 h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
70 h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
71 h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
72
73 return 4294967296 * (2097151 & h2) + (h1 >>> 0);
74 };
75
76 const result = [];
63 results = await session.call("Runtime.evaluate", expression=r"""
77 64 const rootElement = document.querySelector('[decode-data-ved="1"]');
78
79 if (rootElement) {
80 for (const nodes of Array.from(rootElement.querySelectorAll('*')).map(e => Array.from(e.childNodes))) {
81 for (const node of nodes) {
82 result.push(node);
83 }
84 }
85 }
86
87 const lines = result.map(n => n.textContent ? n.textContent.trim() : "");
88 const keepLines = { };
89
90 for (let l of lines) {
91 if (!l) continue;
92 if (l.startsWith('TgQPHd')) continue;
93 const find = l.indexOf("KI-Antworten können Fehler enthalten.");
94 if (find !== -1) {
95 l = l.substring(0, find).trim();
96 if (l) keepLines[cyrb53(l)] = l;
97 break;
65 const allElements = rootElement.querySelectorAll('*');
66 const textNodes = [];
67 Array.from(allElements).forEach(el => {
68 for (const child of el.childNodes) {
69 if (child.nodeType === Node.TEXT_NODE && child.textContent) {
70 const trimedText = child.textContent.trim();
71 if ([
72 "KI-Antworten können Fehler enthalten.",
73 "AI responses may include mistakes."].includes(trimedText)) {
74 break;
75 }
76 textNodes.push(child);
77 }
98 78 }
99
100 // Regex für das Finden und Extrahieren von _setImageSrc('ID', 'BASE64')
101 // Verwendet einfache Anführungszeichen als Begrenzer (wie von Google ausgegeben)
102 const imgMatch = l.match(/_setImageSrc\s*\(\s*'([^']+)'\s*,\s*'([^']+)'\s*\)/);
103
104 if (imgMatch) {
105 // imgMatch[2] ist der Base64-String (data:image/png;base64,...)
106 // Dekodiere Hex-Escapes wie \x3d (=) am Ende des Base64-Strings
107 const base64Data = imgMatch[2].replace(/\\x([0-9a-fA-F]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
108 // Ersetze nur den _setImageSrc-Aufruf, behalte umgebenden Text
109 l = l.replace(imgMatch[0], ''); // `\n![Bild](${base64Data})`);
110 }
111
112 // Behebt den fehlerhaften Regex für die Datei-Metadaten am Zeilenende
113 const fileMatch = l.match(/\\\{.+\\}/);
114 if (fileMatch) {
115 l = l.replace(fileMatch[0], '');
116 }
117
118 const hash = cyrb53(l);
119 keepLines[hash] = l;
120 }
121 Object.values(keepLines);
79 });
80 textNodes.map(n => n.textContent).filter(Boolean);
122 81 """, returnByValue=True);
123 82 debug.log(f"Google Search: AI mode results: {results}")
124 83 if results:
Modified g4f/api/__init__.py +20 -2
@@ -1392,10 +1392,10 @@ class Api:
1392 1392 try:
1393 1393 async with lock:
1394 1394 from g4f.requests.cdp import CDPSession
1395 session = CDPSession(headless=True)
1395 session = CDPSession(headless="&headless=false" not in url)
1396 1396 await session.start()
1397 1397 try:
1398 screenshot_path = await session.capture_screenshot(url)
1398 screenshot_path = await session.capture_screenshot(url, 1 if "q=" in url else 3)
1399 1399 return FileResponse(
1400 1400 screenshot_path,
1401 1401 media_type="image/webp",
@@ -1408,6 +1408,24 @@ class Api:
1408 1408 return ErrorResponse.from_exception(
1409 1409 e, None, HTTP_500_INTERNAL_SERVER_ERROR
1410 1410 )
1411
1412 @self.app.get("/screenshot/{path}", responses=responses)
1413 async def image_from_url(
1414 path: str,
1415 ):
1416 screenshots_dir = os.path.join(get_media_dir(), "screenshots")
1417 for root, _, files in os.walk(screenshots_dir):
1418 for file in files:
1419 if file != path:
1420 continue
1421 if not os.path.isfile(os.path.join(root, file)):
1422 continue
1423 return FileResponse(
1424 os.path.join(root, path),
1425 media_type="image/webp",
1426 headers={"Cache-Control": "max-age=604800"},
1427 )
1428 return ErrorResponse.from_message("File not found", 404)
1411 1429
1412 1430 @self.app.get(
1413 1431 "/v1/providers",
Modified g4f/gui/server/providers.html +0 -1
@@ -245,7 +245,6 @@
245 245
246 246 .detail-grid {
247 247 display: grid;
248 grid-template-columns: 1fr 1fr;
249 248 gap: 1.5rem;
250 249 margin-bottom: 1.5rem;
251 250 }
Modified g4f/gui/server/website.py +51 -16
@@ -8,7 +8,7 @@ import re
8 8 import time
9 9 from datetime import datetime
10 10 from urllib.parse import quote, unquote
11 from flask import send_from_directory, redirect, request
11 from flask import jsonify, send_from_directory, redirect, request
12 12
13 13 from ...files import secure_filename
14 14 from ...cookies import get_cookies_dir
@@ -339,12 +339,12 @@ class Website:
339 339 """
340 340
341 341 # Screenshot / logo section
342 logo_url = f"{p.get('url', (p.get('base_url', p.get('baseUrl', '')))).replace('playground.ai.', '').replace('https://', '').replace('http://', '').replace('api.', '').replace('console.', '').replace('api.', '').replace('router.', '').split('/')[0]}"
342 logo_url = f"{(p.get('url', (p.get('base_url', p.get('baseUrl', '')))) or "").replace('playground.ai.', '').replace('https://', '').replace('http://', '').replace('api.', '').replace('console.', '').replace('api.', '').replace('router.', '').split('/')[0]}"
343 343 logo_url = f"api.airforce" if logo_url == "airforce" else logo_url
344 logo_url = f"/screenshot?url=https://{logo_url}"
344 create_url = f"/screenshot?url=https://{logo_url}"
345 345 screenshot_html = f"""
346 346 <div class="screenshot-section">
347 <img data-src="{logo_url}" alt="{escape(p['name'])} logo" class="provider-logo"
347 <img src="/screenshot/{logo_url}" data-src="{create_url}" alt="{escape(p['name'])} logo" class="provider-logo"
348 348 style="max-width:100%;border-radius:8px;border:1px solid var(--card-border)" />
349 349 <p class="screenshot-caption">Load screenshot from {escape(p['url'] or 'N/A')}</p>
350 350 </div>
@@ -375,13 +375,15 @@ class Website:
375 375 'groq', 'Groq');
376 376 return img;
377 377 }}
378 const img = document.querySelector('img[data-src="{logo_url}"]');
378 const img = document.querySelector('img[data-src="{create_url}"]');
379 const input = document.createElement('input');
379 380 const previewImg = getImage("{p['name']}")
380 381 img.parentElement.appendChild(previewImg);
381 382 let n = 1;
382 383 let previewRemoved = false;
383 const orgSrc = img.dataset.src;
384 const createSrc = img.dataset.src;
384 385 img.onload = () => {{
386 input.placeholder = 'Ask {escape(p.get("label", p["name"]))}';
385 387 if (!previewRemoved && previewImg.parentNode) {{
386 388 previewImg.parentNode.removeChild(previewImg);
387 389 previewRemoved = true;
@@ -389,20 +391,57 @@ class Website:
389 391 n = n + 1;
390 392 if (n <= 3) {{
391 393 setTimeout(() => {{
392 img.src = orgSrc + `_${{n}}.webp`;
394 img.src = createSrc + `_${{n}}.webp`;
393 395 }}, 1000);
394 396 }}
395 397 }};
396 398 img.onerror = () => {{
399 input.placeholder = 'Ask {escape(p.get("label", p["name"]))}';
400 if (img.src.includes(createSrc)) {{
401 img.parentNode.removeChild(img);
402 return;
403 }}
397 404 if (n === 1) {{
398 img.src = 'https://image.thum.io/get/width/600/{logo_url}';
405 img.src = createSrc;
399 406 return;
400 407 }}
401 if (img.src == orgSrc) return;
402 408 n = 3; // Stop carousel on error
403 img.src = orgSrc;
409 img.src = createSrc;
404 410 }};
405 img.src = img.dataset.src;
411 input.type = 'text';
412 input.placeholder = 'Ask {escape(p.get("label", p["name"]))}';
413 input.className = 'provider-input';
414 input.addEventListener('change', function(event) {{
415 if (!event.target.value) {{
416 img.src = createSrc;
417 return;
418 }}
419 let newUrl = '';
420 if ('{p["name"]}' == 'YouTube') {{
421 const createUrl = new URL('{create_url}', location.origin);
422 const queryUrl = new URL(createUrl.searchParams.get('url'));
423 queryUrl.pathname = "/results";
424 queryUrl.searchParams.set('search_query', event.target.value);
425 newUrl = "/screenshot?url=" + encodeURIComponent(queryUrl.toString());
426 }} else if (['GoogleSearch', 'GoogleAiMode'].includes('{p["name"]}')) {{
427 const createUrl = new URL('{create_url}', location.origin);
428 const queryUrl = new URL(createUrl.searchParams.get('url'));
429 queryUrl.pathname = "/search";
430 const appendUrl = queryUrl.toString() + (queryUrl.toString().includes('?') ? '&q=' : '?q=') + event.target.value;
431 newUrl = "/screenshot?url=" + encodeURIComponent(appendUrl);
432 }} else {{
433 newUrl = createSrc + encodeURIComponent('?q=' + event.target.value);
434 }}
435 if (img.src !== newUrl) {{
436 img.src = newUrl;
437 }}
438 input.value = '';
439 input.placeholder = 'Is Loading...';
440 }});
441 const inputContainer = document.createElement('div');
442 inputContainer.className = 'provider-input-container';
443 inputContainer.appendChild(input);
444 img.parentElement.appendChild(inputContainer);
406 445 </script>
407 446 """
408 447
@@ -485,10 +524,6 @@ class Website:
485 524 def _apps(self, filename: str = "index.html"):
486 525 return render(f"apps/{filename}")
487 526
488 def _sillytavern(self, filename: str = "index.html"):
489 SILLYTAVERN_URL = "https://raw.githubusercontent.com/SillyTavern/SillyTavern/refs/heads/release/"
490 return render(f"public/{filename}", SILLYTAVERN_URL)
491
492 527 def _playground(self, filename: str = "index.html"):
493 528 PLAYGROUND_URL = (
494 529 "https://raw.githubusercontent.com/gpt4free/playground/refs/heads/main/"
@@ -509,7 +544,7 @@ class Website:
509 544 cache_dir = os.path.join(get_cookies_dir(), ".playground_cache")
510 545 safe_path = os.path.normpath(os.path.join(cache_dir, filename))
511 546 if not safe_path.startswith(cache_dir + os.sep) and safe_path != cache_dir:
512 return redirect("/playground/")
547 return jsonify({"error": "Invalid filename"}), 400
513 548 # Serve from cache if present
514 549 if os.path.isfile(safe_path):
515 550 return send_from_directory(
Modified g4f/requests/cdp.py +133 -26
@@ -674,31 +674,34 @@ class CDPSession:
674 674 async def click_accept_button(self) -> bool:
675 675 """Find and click an 'Accept' or 'Einwilligen' button, including inside iframes."""
676 676 js_code = """
677 (() => {
678 const targetTexts = ['Accept', 'Accept all', 'Accept All', 'Einwilligen', 'Alle akzeptieren', 'Zustimmen und weiter', 'Zustimmen'];
679
677 // 1. Inject debug script to show logging
678 const debugEl = document.createElement('script');
679 debugEl.src = 'https://g4f.dev/dist/js/debug.js';
680 document.head.appendChild(debugEl);
681
682 // 2. Get the current URL's search parameters
683 const params = new URLSearchParams(window.location.search);
684 const searchQuery = params.get('q');
685
686 // 3. Click any "Accept" button in the main document or nested iframes
687 const targetTexts = [
688 'Send', 'Accept', 'Accept all', 'Accept All',
689 'Accept All Cookies', 'Accept all cookies',
690 'Einwilligen', 'Alle akzeptieren',
691 'Zustimmen und weiter', 'Zustimmen'
692 ];
693 const acceptBtns = (() => {
680 694 function searchDocument(doc, offsetX = 0, offsetY = 0) {
695 const foundButtons = [];
681 696 try {
682 if (!doc) return null;
697 if (!doc) return [];
683 698
684 699 // 1. Search buttons in the current document
685 const buttons = doc.querySelectorAll('button, input[type="submit"], [role="button"]');
700 const buttons = doc.querySelectorAll('button, input[type="submit"], [role="button"], a');
686 701 for (let button of buttons) {
687 702 const text = (button.innerText || button.value || button.textContent || '').trim();
688 703 if (targetTexts.includes(text)) {
689
690 // NEU: Scrollt das Element/den Container in den sichtbaren Bereich
691 button.scrollIntoView({ block: 'center', inline: 'center' });
692
693 // Wichtig: Nach dem Scrollen müssen die Koordinaten neu berechnet werden!
694 const rect = button.getBoundingClientRect();
695
696 if (rect.width > 0 && rect.height > 0) {
697 return [
698 offsetX + rect.left + rect.width / 2,
699 offsetY + rect.top + rect.height / 2
700 ];
701 }
704 foundButtons.push(button);
702 705 }
703 706 }
704 707
@@ -709,24 +712,125 @@ class CDPSession:
709 712 const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
710 713 if (iframeDoc) {
711 714 const iframeRect = iframe.getBoundingClientRect();
712 const res = searchDocument(
715 const btns = searchDocument(
713 716 iframeDoc,
714 717 offsetX + iframeRect.left,
715 718 offsetY + iframeRect.top
716 719 );
717 if (res) return res;
720 if (btns.length > 0) {
721 foundButtons.push(...btns);
722 }
718 723 }
719 724 } catch (e) {
720 725 // Cross-origin iframe security restriction
721 726 }
722 727 }
723 } catch (e) {}
724 return null;
728 } catch (e) {
729 console.error('Error searching for accept buttons:', e);
730 }
731 return foundButtons;
725 732 }
726 733
727 // window.scrollX/Y wird am Ende aufgeschlagen, falls du absolute Page-Koordinaten brauchst
728 734 return searchDocument(document, window.scrollX, window.scrollY);
729 })()
735 })();
736 if (acceptBtns && acceptBtns.length > 0) {
737 acceptBtns.forEach(btn => {
738 try {
739 btn.click();
740 } catch (e) {
741 console.error('Failed to click accept button:', e);
742 }
743 });
744 }
745
746 // 4. Enable Google AI Mode if the URL has the ai-mode parameter
747 function enableGoogleAiMode() {
748 // Enable Google AI Mode if the URL has the ai-mode parameter
749 const aiMode = params.has('ai-mode');
750 if (aiMode) {
751 const b = Array.from(document.querySelectorAll("a, button")).filter(a => {
752 return a.textContent.endsWith("KI‑Modus") || a.textContent.endsWith("AI Mode");
753 }).pop();
754 b ? b.click() : null;
755 setTimeout(() => {
756 b ? b.click() : null;
757 }, 1000);
758 }
759 }
760 enableGoogleAiMode();
761
762 // 5. Find the textarea
763 const textarea = document.querySelector('textarea[name="prompt"], [class^="MessageInput__TextArea--"], [placeholder="Type a message..."]');
764
765 // 6. Only proceed if we found a query and the textarea exists
766 if (searchQuery && textarea) {
767 // Set the value
768 textarea.value = searchQuery;
769
770 // Dispatch an 'input' event to notify the page that the value has changed
771 // This is crucial for frameworks like React/Vue to recognize the update
772 const event = new Event('input', { bubbles: true });
773 textarea.dispatchEvent(event);
774
775 // Optional: dispatch 'change' event as well, in case the site relies on it
776 textarea.dispatchEvent(new Event('change', { bubbles: true }));
777
778 enableGoogleAiMode();
779 }
780
781 // 7. Handle special cases for specific sites (like DeepSeek, Gemini, etc.)
782 (function() {
783 // 1. Target the specific element Kimi uses
784 // Inspect the page; if it's the main input, it might be a div with contenteditable
785 const editor = document.querySelector('[contenteditable="true"], [placeholder="Message DeepSeek"], .message-input-textarea');
786 if (!editor) return;
787
788 // 2. Get your query
789 const searchQuery = new URLSearchParams(window.location.search).get('q');
790 if (!searchQuery) return;
791
792 // 3. Focus the element first (some frameworks require this)
793 editor.focus();
794
795 // 4. Use the document.execCommand approach
796 // This simulates real user typing and is the most likely way to trigger framework state
797 document.execCommand('selectAll', false, null);
798 document.execCommand('insertText', false, searchQuery);
799
800 // 5. If that fails, force React/Vue state update
801 // This triggers the underlying setter that frameworks use
802 const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
803 window.HTMLElement.prototype,
804 'innerText'
805 ).set;
806
807 nativeInputValueSetter.call(editor, searchQuery);
808
809 // Dispatch events to notify the framework
810 editor.dispatchEvent(new Event('input', { bubbles: true }));
811 editor.dispatchEvent(new Event('change', { bubbles: true }));
812
813 enableGoogleAiMode();
814 })();
815
816
817 // 8. Click the send button if it exists
818 document.querySelector('[data-send-label="Send message"],'
819 + ' [aria-label="Send message"], '
820 + '[class^="MessageInput__Submit--"], '
821 + ' .send-button-container, .send-button')?.click();
822
823 // 8. Click the send button on gemini.google.com
824 const trigger = (el, etype) => {
825 const event = new Event( etype, { bubbles: true } );
826 el.dispatchEvent( event );
827 };
828 setTimeout(() =>
829 trigger(document.querySelector(`.send-button`), `click`),
830 1000);
831
832 // 8. Click the send button on chat.deepseek.com
833 document.querySelector('[style="width: fit-content;"] [role="button"]')?.click();
730 834 """
731 835 try:
732 836 rect = await self.evaluate_js(js_code)
@@ -821,14 +925,17 @@ class CDPSession:
821 925 # Wait for network activity to settle before capturing
822 926 await self.wait_for_network_idle(idle_time=5, timeout=15.0)
823 927 # Try to click any "Accept" or "Einwilligen" cookie consent buttons
824 if n != 1:
928 if n < 3:
825 929 for _ in range(2):
826 930 debug.log("Attempting to click accept button...")
827 931 await asyncio.sleep(1)
828 932 if await self.click_accept_button():
829 933 debug.log("Clicked accept button.")
830 await asyncio.sleep(1)
831 934 break
935 print(url_without_suffix)
936 if ("&headless=false" in url_without_suffix or "&sleep=" in url_without_suffix or "&wait=" in url_without_suffix) and n == 3:
937 debug.log("Waiting 5 seconds for page to settle due to sleep/wait parameter...")
938 await asyncio.sleep(30)
832 939 await self.wait_for_network_idle(idle_time=5, timeout=15.0)
833 940 result = await self.call("Page.captureScreenshot")
834 941 image_bytes = base64.b64decode(result["data"])