返回提交历史
Modified
g4f/Provider/search/GoogleAiMode.py
+20
-61
Modified
g4f/api/__init__.py
+20
-2
Modified
g4f/gui/server/providers.html
+0
-1
Modified
g4f/gui/server/website.py
+51
-16
Modified
g4f/requests/cdp.py
+133
-26
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
代码差异
5 个文件
+224
-106
@@ -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`);
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:
@@ -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",
@@ -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
}
@@ -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(
@@ -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"])