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

XFEstudio/gpt4free

Add AuthManager import to Ollama and scroll to top before screenshot capture

5c1c143d
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

4 个文件 +60 -315
Modified g4f/Provider/local/Ollama.py +1 -0
@@ -12,6 +12,7 @@ from ...cookies import get_cookies
12 12 from ...typing import AsyncResult, Messages
13 13 from ...config import AppConfig
14 14 from ...errors import MissingAuthError
15 from ...tools.auth import AuthManager
15 16
16 17
17 18 class Ollama(OpenaiTemplate):
Modified g4f/Provider/template/OpenaiTemplate.py +1 -3
@@ -51,9 +51,7 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
51 51 return await super().get_quota(api_key=api_key, **kwargs)
52 52 if not api_key and cls.needs_auth:
53 53 raise MissingAuthError("API key is required.")
54 if not cls.default_model and not cls.backup_url:
55 raise NotImplementedError("No default model specified.")
56 return await cls.test_api_key(api_key)
54 raise NotImplementedError("Quota URL is not defined for this provider.")
57 55
58 56 @classmethod
59 57 async def test_api_key(cls, api_key: str):
Modified g4f/api/__init__.py +31 -10
@@ -132,6 +132,36 @@ except ImportError:
132 132
133 133 logger = logging.getLogger(__name__)
134 134
135 V1_LANDING_PAGE = """<!doctype html>
136 <html lang="en">
137 <head>
138 <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
139 <meta name="theme-color" content="#101411"><title>g4f /v1 API</title>
140 <style>
141 :root{--bg:#101411;--panel:#171d18;--ink:#f5f4ed;--muted:#a7afa4;--line:#344138;--lime:#c4f06c;--coral:#ff816d;--mono:ui-monospace,SFMono-Regular,Consolas,monospace}
142 *{box-sizing:border-box}body{margin:0;color:var(--ink);background:var(--bg);font:16px/1.55 Georgia,serif}body:before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.2;background-image:linear-gradient(#344138 1px,transparent 1px),linear-gradient(90deg,#344138 1px,transparent 1px);background-size:42px 42px;mask-image:linear-gradient(#000,transparent 72%)}a{color:var(--lime)}.wrap{width:min(1120px,calc(100% - 40px));margin:auto;position:relative}
143 header{display:flex;justify-content:space-between;align-items:center;padding:28px 0;border-bottom:1px solid var(--line)}.brand{color:var(--ink);text-decoration:none;font:700 18px/1 var(--mono);letter-spacing:-.04em}.brand b{color:var(--lime)}nav{display:flex;gap:22px;font:13px var(--mono)}nav a{color:var(--muted);text-decoration:none}nav a:hover{color:var(--lime)}
144 .hero{padding:96px 0 88px;display:grid;grid-template-columns:1.2fr .8fr;gap:70px;align-items:end}.eyebrow{color:var(--coral);font:12px var(--mono);letter-spacing:.12em;text-transform:uppercase}h1{max-width:720px;margin:20px 0;font-size:clamp(48px,8vw,94px);line-height:.94;letter-spacing:-.065em;font-weight:400}.lede{max-width:590px;color:var(--muted);font-size:20px}.hero-aside{border-left:2px solid var(--lime);padding-left:22px;color:var(--muted)}.hero-aside strong{display:block;color:var(--ink);font:700 14px var(--mono);margin-bottom:12px}.actions{display:flex;flex-wrap:wrap;gap:12px;margin-top:30px}.button{display:inline-block;padding:11px 16px;border:1px solid var(--lime);color:var(--bg);background:var(--lime);font:700 13px var(--mono);text-decoration:none}.button.alt{color:var(--lime);background:transparent}
145 section{padding:28px 0 92px}.section-head{display:flex;justify-content:space-between;align-items:baseline;gap:20px;margin-bottom:24px;border-bottom:1px solid var(--line);padding-bottom:14px}h2{margin:0;font-size:28px;font-weight:400;letter-spacing:-.04em}.section-head span{color:var(--muted);font:12px var(--mono)}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.card{display:block;padding:23px;border:1px solid var(--line);background:rgba(23,29,24,.88);min-height:174px;text-decoration:none}.card:hover{border-color:var(--lime)}.card h3{margin:0 0 8px;color:var(--ink);font-size:21px;font-weight:400}.card p{margin:0 0 18px;color:var(--muted)}code,pre{font-family:var(--mono)}.endpoint{color:var(--lime);font-size:12px}
146 .code-box{position:relative;border:1px solid var(--line);background:#0b0e0c;padding:24px;overflow:auto}pre{margin:0;color:#d9e5d2;font-size:13px;line-height:1.7}.token{color:var(--coral)}.copy{position:absolute;top:12px;right:12px;border:1px solid var(--line);color:var(--lime);background:transparent;padding:7px 10px;cursor:pointer;font:11px var(--mono)}footer{padding:22px 0 42px;color:var(--muted);border-top:1px solid var(--line);font:12px var(--mono)}
147 @media(max-width:720px){.wrap{width:min(100% - 28px,560px)}header{align-items:flex-start;gap:18px}nav{gap:10px;flex-wrap:wrap;justify-content:flex-end}.hero{padding:68px 0 58px;display:block}h1{font-size:clamp(50px,16vw,78px)}.hero-aside{margin-top:42px}.grid{grid-template-columns:1fr}section{padding-bottom:60px}}
148 </style></head>
149 <body><div class="wrap">
150 <header><a class="brand" href="/"><b>g4f</b> / api</a><nav><a href="/v1/models">models</a><a href="/docs">swagger</a><a href="/redoc">redoc</a></nav></header>
151 <main><div class="hero"><div><div class="eyebrow">Unified inference gateway · v1</div><h1>One endpoint.<br>Many minds.</h1><p class="lede">Build with chat, reasoning, vision, image, audio, and Anthropic-compatible interfaces through a single g4f API.</p><div class="actions"><a class="button" href="/docs">Open API reference</a><a class="button alt" href="/v1/models">Browse models</a></div></div><aside class="hero-aside"><strong>BASE URL</strong><code>/v1</code><p>Drop it into an OpenAI client, keep your existing request shape, and switch models without rewriting your application.</p></aside></div>
152 <section><div class="section-head"><h2>Interfaces</h2><span>three ways in</span></div><div class="grid">
153 <a class="card" href="/docs"><h3>Chat completions</h3><p>OpenAI-compatible messages with streaming, tools, vision, and model selection.</p><span class="endpoint">POST /v1/chat/completions</span></a>
154 <a class="card" href="/docs"><h3>Responses</h3><p>A modern response interface for multi-turn reasoning and richer output types.</p><span class="endpoint">POST /v1/responses</span></a>
155 <a class="card" href="/docs"><h3>Messages</h3><p>Anthropic-style requests for teams moving between compatible providers.</p><span class="endpoint">POST /v1/messages</span></a>
156 <a class="card" href="/docs"><h3>Media generation</h3><p>Generate images and other supported media from the same API surface.</p><span class="endpoint">POST /v1/media/generate</span></a>
157 <a class="card" href="/v1/models"><h3>Model catalog</h3><p>Inspect the models and provider options available on this server right now.</p><span class="endpoint">GET /v1/models</span></a>
158 <a class="card" href="/docs"><h3>Live schema</h3><p>Try requests in your browser and inspect the generated OpenAPI contract.</p><span class="endpoint">GET /docs</span></a>
159 </div></section><section><div class="section-head"><h2>Start here</h2><span>curl · JSON · streamable</span></div><div class="code-box"><button class="copy" onclick="copyExample(this)">copy</button><pre id="example">curl -X POST /v1/chat/completions \\
160 -H 'Content-Type: application/json' \\
161 -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hello"}]}'</pre></div></section></main>
162 <footer>g4f API · <a href="/openapi.json">openapi.json</a> · Compatible clients welcome.</footer></div>
163 <script>function copyExample(button){navigator.clipboard?.writeText(document.getElementById("example").textContent).then(()=>{button.textContent="copied";setTimeout(()=>button.textContent="copy",1400)})}</script></body></html>"""
164
135 165 # ---------------------------------------------------------------------------
136 166 # Request / response log store
137 167 # ---------------------------------------------------------------------------
@@ -669,16 +699,7 @@ class Api:
669 699
670 700 @self.app.get("/v1")
671 701 async def read_root_v1():
672 return HTMLResponse(
673 "g4f API: Go to "
674 '<a href="/v1/models">models</a>, '
675 '<a href="/v1/chat/completions">chat/completions</a>, '
676 '<a href="/v1/responses">responses</a> (OpenAI), '
677 '<a href="/v1/messages">messages</a> (Anthropic), or '
678 '<a href="/v1/media/generate">media/generate</a> <br><br>'
679 "Open Swagger UI at: "
680 '<a href="/docs">/docs</a>'
681 )
702 return HTMLResponse(V1_LANDING_PAGE)
682 703
683 704 @self.app.get(
684 705 "/v1/models",
Modified g4f/requests/cdp.py +27 -302
@@ -1011,6 +1011,7 @@ if (deepseekSendButton) {
1011 1011 raise RuntimeError(f"Failed to load page {url} for screenshot, document.doctype={await self.evaluate_js('String(document.doctype)')}")
1012 1012
1013 1013 await self.bypass_turnstile()
1014 await self.evaluate_js("window.scrollTo(0, 0);")
1014 1015
1015 1016 result = None
1016 1017 for i in range(n):
@@ -1022,6 +1023,31 @@ if (deepseekSendButton) {
1022 1023 except Exception as e:
1023 1024 debug.log(f"Screenshot #{i+1} failed: {e}")
1024 1025 return result
1026
1027 # response = await self.evaluate_js("""
1028 # new Promise((resolve, reject) => {
1029 # const html2canvasEL = document.createElement('script');
1030 # html2canvasEL.src = 'https://html2canvas.hertzen.com/dist/html2canvas.min.js';
1031 # html2canvasEL.onload = async () => {
1032 # c=await html2canvas(document.body, {/*width: 1200, height: 630*/});
1033 # c.toBlob(async (b)=>{
1034 # const url = "https://media.pollinations.ai/upload";
1035 # const formData = new FormData();
1036 # formData.append('file', b);
1037 # const response = await fetch(url, {
1038 # method: 'POST',
1039 # body: formData,
1040 # headers: {"Authorization": "Bearer pk_7X0QLj0xijSd0xj7"}
1041 # });
1042 # resolve(await response.json())
1043 # }, 'image/webp');
1044 # };
1045 # html2canvasEL.onerror = (e) => { reject(e); };
1046 # document.head.appendChild(html2canvasEL);
1047 # """)
1048 # async with aiohttp.ClientSession() as session:
1049 # async with session.get(response['url']) as resp:
1050 # image_bytes = await resp.read()
1025 1051
1026 1052 async def _capture_screenshot_impl(self, url: str, n: int) -> str:
1027 1053 url_without_suffix = url[:-7] if url.endswith("_2.webp") or url.endswith("_3.webp") else url
@@ -1106,305 +1132,4 @@ if (deepseekSendButton) {
1106 1132 self.target_id = None
1107 1133
1108 1134 # Release our tab; browser stays alive for reuse by other tabs
1109 release_shared_browser_ref()
1110
1111
1112 class SyncCDPSession:
1113 """
1114 Synchronous (blocking) CDP client for use in Turnstile-solving providers
1115 (e.g. DeepInfra) where async timeout-based approach is unreliable.
1116
1117 Unlike CDPSession which uses asyncio+aiohttp and futures with timeouts,
1118 this class uses a simple blocking while-loop recv() model — it will wait
1119 as long as the browser needs without the risk of a premature TimeoutError.
1120
1121 Requires: pip install websocket-client
1122
1123 Example Usage (run from an async context via executor):
1124 def run_sync():
1125 session = SyncCDPSession(port=12345, headless=False)
1126 session.start_chrome()
1127 try:
1128 session.navigate("https://example.com")
1129 title = session.evaluate_js("document.title")
1130 return title
1131 finally:
1132 session.close()
1133
1134 result = await asyncio.get_event_loop().run_in_executor(None, run_sync)
1135 """
1136
1137 def __init__(
1138 self,
1139 port: Optional[int] = None,
1140 host: Optional[str] = None,
1141 user_data_dir: Optional[str] = None,
1142 headless: bool = None,
1143 ):
1144 if port is None:
1145 port = BrowserConfig.port
1146 if host is None:
1147 host = BrowserConfig.host
1148 if host is None:
1149 host = "127.0.0.1"
1150 if headless is None:
1151 headless = BrowserConfig.headless
1152 self.port = port
1153 self.host = host
1154 self.headless = headless
1155 self.user_data_dir = (
1156 user_data_dir # Ignored if using shared pool, but kept for compatibility
1157 )
1158 self.process = None
1159 self.ws = None
1160 self.target_id = None
1161 self.id_counter = 0
1162
1163 # Network event loggers
1164 self.network_requests: List[dict] = []
1165 self.network_responses: List[dict] = []
1166
1167 def start_chrome(self):
1168 """Launch/get shared Chrome and connect via CDP targeting a new tab."""
1169 if self.port is None:
1170 self.port = get_shared_browser(self.host, self.port, self.headless)
1171
1172 # Acquire a reference so the shared browser stays alive for this tab
1173 acquire_shared_browser_ref()
1174
1175 # Create a new tab target
1176 ws_url = None
1177 for _ in range(10):
1178 try:
1179 req = urllib.request.Request(
1180 f"http://{self.host}:{self.port}/json/new", method="PUT"
1181 )
1182 with urllib.request.urlopen(req, timeout=2) as response:
1183 target = json.loads(response.read().decode("utf-8"))
1184 ws_url = target.get("webSocketDebuggerUrl")
1185 self.target_id = target.get("id")
1186 if ws_url:
1187 break
1188 except Exception:
1189 time.sleep(0.5)
1190
1191 if not ws_url:
1192 release_shared_browser_ref()
1193 raise RuntimeError(f"Failed to create new tab target on port {self.port}")
1194
1195 self._connect(ws_url)
1196
1197 def _connect(self, ws_url: str):
1198 """Connect via WebSocket to the target."""
1199 try:
1200 from websocket import create_connection
1201 except ImportError:
1202 raise ImportError(
1203 'Install "websocket-client" package: pip install websocket-client'
1204 )
1205
1206 self.ws = create_connection(ws_url)
1207 self.ws.settimeout(60) # Prevent infinite hang if Chrome crashes mid-call
1208
1209 # Enable essential CDP domains
1210 self.call("Page.enable")
1211 self.call("DOM.enable")
1212 self.call("Runtime.enable")
1213 self.call("Network.enable")
1214 self.call("Emulation.setFocusEmulationEnabled", enabled=True)
1215
1216 def call(self, method: str, **params) -> dict:
1217 """Send a CDP command and block until the matching response arrives, logging events."""
1218 self.id_counter += 1
1219 payload = {"id": self.id_counter, "method": method, "params": params}
1220 self.ws.send(json.dumps(payload))
1221
1222 # Blocking loop with a 60s socket timeout — won't hang forever if browser exits
1223 while True:
1224 response = json.loads(self.ws.recv())
1225 if "id" in response:
1226 if response.get("id") == self.id_counter:
1227 if "error" in response:
1228 raise RuntimeError(
1229 f"CDP error in {method}: {response['error']}"
1230 )
1231 return response.get("result", {})
1232 else:
1233 # Event
1234 event_method = response.get("method")
1235 event_params = response.get("params", {})
1236 if event_method == "Network.requestWillBeSent":
1237 self.network_requests.append(event_params)
1238 elif event_method == "Network.responseReceived":
1239 self.network_responses.append(event_params)
1240
1241 def evaluate_js(self, expression: str) -> Any:
1242 """Execute JS on the page and return the primitive result value."""
1243 res = self.call("Runtime.evaluate", expression=expression, returnByValue=True)
1244 return res.get("result", {}).get("value")
1245
1246 def get_cookies(self) -> dict:
1247 """Retrieve all cookies from the browser as a name-value dict."""
1248 cookies = self.get_cookies_list()
1249 return {c["name"]: c["value"] for c in cookies}
1250
1251 def get_cookies_list(self, urls: Optional[List[str]] = None) -> List[dict]:
1252 """Retrieve full cookie objects from the browser session."""
1253 params = {}
1254 if urls:
1255 params["urls"] = urls
1256 res = self.call("Network.getCookies", **params)
1257 return res.get("cookies", [])
1258
1259 def set_cookies(self, cookies: List[dict]):
1260 """Set cookies in the browser session."""
1261 for cookie in cookies:
1262 params = {
1263 "name": cookie.get("name"),
1264 "value": cookie.get("value"),
1265 "domain": cookie.get("domain"),
1266 "path": cookie.get("path"),
1267 "secure": cookie.get("secure"),
1268 "httpOnly": cookie.get("httpOnly"),
1269 "sameSite": cookie.get("sameSite"),
1270 "expires": cookie.get("expires"),
1271 }
1272 params = {k: v for k, v in params.items() if v is not None}
1273 if "domain" not in params and "url" not in params:
1274 params["url"] = "https://deepinfra.com"
1275 self.call("Network.setCookie", **params)
1276
1277 def navigate(self, url: str):
1278 """Navigate to a URL and wait for initial load."""
1279 self.call("Page.navigate", url=url)
1280 time.sleep(2.0)
1281
1282 def wait_for_network_idle(self, idle_time: float = 0.5, timeout: float = 15.0) -> bool:
1283 """Wait until network activity settles (no in-flight requests for *idle_time* seconds).
1284
1285 Polls document.readyState and the Performance Resource Timing API to detect
1286 when resource loading has stabilised. Returns True when idle, False on timeout.
1287 """
1288 deadline = time.monotonic() + timeout
1289 last_count = -1
1290 stable_since = time.monotonic()
1291
1292 while time.monotonic() < deadline:
1293 try:
1294 ready = self.evaluate_js("document.readyState")
1295 if ready == "complete":
1296 # Count resources that are still loading (responseStart > 0 but no responseEnd)
1297 count = self.evaluate_js(
1298 """(() => {
1299 const entries = performance.getEntriesByType('resource');
1300 let pending = 0;
1301 for (const e of entries) {
1302 if (e.responseStart > 0 && e.responseEnd === 0) {
1303 pending++;
1304 }
1305 }
1306 return pending;
1307 })()"""
1308 )
1309 count = count or 0
1310 if count == last_count:
1311 if (time.monotonic() - stable_since) >= idle_time:
1312 return True
1313 else:
1314 last_count = count
1315 stable_since = time.monotonic()
1316 else:
1317 # Page not fully loaded yet — reset stability timer
1318 last_count = -1
1319 stable_since = time.monotonic()
1320 except Exception:
1321 pass
1322 time.sleep(0.2)
1323
1324 return False
1325
1326 def click(self, x: int = 200, y: int = 400):
1327 """
1328 Simulate a real mouse click at (x, y) on the page.
1329
1330 Gives the page window focus, which signals Cloudflare that a real user
1331 is present. This significantly speeds up Turnstile token generation.
1332 Call this once after navigate(), before polling for the token.
1333 """
1334 self.call(
1335 "Input.dispatchMouseEvent",
1336 type="mousePressed",
1337 x=x,
1338 y=y,
1339 button="left",
1340 clickCount=1,
1341 )
1342 self.call(
1343 "Input.dispatchMouseEvent",
1344 type="mouseReleased",
1345 x=x,
1346 y=y,
1347 button="left",
1348 clickCount=1,
1349 )
1350
1351 def close(self):
1352 """Close WebSocket session and close this tab only.
1353
1354 The shared browser process is kept alive as long as other CDP sessions
1355 (tabs) are active. When the last session releases its reference the
1356 browser is terminated automatically.
1357 """
1358 if self.ws:
1359 try:
1360 self.ws.close()
1361 except Exception:
1362 pass
1363 self.ws = None
1364
1365 if self.target_id and self.port:
1366 try:
1367 urllib.request.urlopen(
1368 f"http://{self.host}:{self.port}/json/close/{self.target_id}",
1369 timeout=2,
1370 )
1371 except Exception:
1372 pass
1373 self.target_id = None
1374
1375 # Release our tab; browser stays alive for reuse by other tabs
1376 release_shared_browser_ref()
1377
1378 def capture_screenshot(self, url: str) -> bytes:
1379 """Navigate to a URL and capture a screenshot, caching the result."""
1380 datekey = datetime.date.today().isoformat()
1381 screenshots_dir = get_screenshot_dir(datekey)
1382 filename = f"{secure_filename(url)}.webp"
1383 filepath = os.path.join(screenshots_dir, filename)
1384
1385 if os.path.exists(filepath):
1386 return Path(filepath).read_bytes()
1387
1388 self.navigate(url)
1389 # Wait for network activity to settle before capturing
1390 self.wait_for_network_idle()
1391 # Try to click any "Accept" or "Einwilligen" cookie consent buttons
1392 self.click_accept_button()
1393 time.sleep(0.5)
1394 result = self.call("Page.captureScreenshot")
1395 image_bytes = base64.b64decode(result["data"])
1396
1397 # Resize to 1200x675 and save as WebP to reduce file size
1398 if has_pillow:
1399 from io import BytesIO
1400 image = Image.open(BytesIO(image_bytes))
1401 image = image.resize((1200, 675), Image.Resampling.LANCZOS)
1402 width, height = image.size
1403 image = image.crop((0, 0, max(0, width - 10), height))
1404 image = image.convert("RGB")
1405 output = BytesIO()
1406 image.save(output, format="WEBP", quality=85, method=6)
1407 image_bytes = output.getvalue()
1408
1409 Path(filepath).write_bytes(image_bytes)
1410 return image_bytes
1135 release_shared_browser_ref()