import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import '../http_client.dart'; import '../http_util.dart'; import '../models.dart'; import 'base_provider.dart'; /// Cookie to plant on the origin before the page loads (Python fake Copilot cookies). class CookieSeed { const CookieSeed({ required this.name, required this.value, this.domain, this.path = '/', this.isSecure = true, }); final String name; final String value; final String? domain; final String path; final bool isSecure; } /// Load [originUrl] in a real Chromium WebView and run async JS on that origin. /// /// This is the Android analog of g4f nodriver: same-origin `fetch` / WebSocket /// after Cloudflare, not a generic fetch from about:blank (CORS → `{}`). /// /// `evaluateJavascript` does not await Promises on Android and stringifies /// them as `{}`. Always use `callAsyncJavaScript`. Future runOriginWebView({ required String originUrl, required String functionBody, Duration timeout = const Duration(seconds: 70), List seeds = const [], }) async { if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS && !Platform.isMacOS && !Platform.isWindows)) { throw UnsupportedError('Origin WebView is not supported on this platform.'); } final originUri = Uri.parse(originUrl); final cookieManager = CookieManager.instance(); for (final seed in seeds) { try { await cookieManager.setCookie( url: WebUri(originUrl), name: seed.name, value: seed.value, domain: seed.domain ?? originUri.host, path: seed.path, isSecure: seed.isSecure, ); } catch (e) { debugPrint('WebView setCookie ${seed.name}: $e'); } } final completer = Completer(); var started = false; HeadlessInAppWebView? view; Future importCookies() async { try { final cookies = await cookieManager.getCookies(url: WebUri(originUrl)); for (final c in cookies) { g4fCookies.set( name: c.name, value: '${c.value}', domain: c.domain ?? originUri.host, path: c.path ?? '/', secure: c.isSecure ?? originUri.scheme == 'https', hostOnly: c.domain == null || c.domain == originUri.host, ); } } catch (e) { debugPrint('WebView import cookies: $e'); } } Future runScript( InAppWebViewController controller, { bool force = false, }) async { if (started || completer.isCompleted) return; final title = (await controller.getTitle() ?? '').toLowerCase(); final challenge = title.contains('just a moment') || title.contains('attention required') || title.contains('cloudflare'); if (challenge && !force) return; started = true; try { await Future.delayed(const Duration(milliseconds: 800)); await importCookies(); final result = await controller.callAsyncJavaScript( functionBody: functionBody, ); if (result?.error != null && result!.error!.trim().isNotEmpty) { throw Exception(result.error); } final value = result?.value; if (value == null) { throw Exception('WebView script returned null'); } final raw = value is String ? value : jsonEncode(value); if (!completer.isCompleted) completer.complete(raw); } catch (e) { if (!completer.isCompleted) completer.completeError(e); } } view = HeadlessInAppWebView( initialUrlRequest: URLRequest(url: WebUri(originUrl)), initialSettings: InAppWebViewSettings( javaScriptEnabled: true, domStorageEnabled: true, thirdPartyCookiesEnabled: true, incognito: false, userAgent: kDesktopUserAgent, isFraudulentWebsiteWarningEnabled: false, cacheEnabled: true, ), onLoadStop: (controller, url) async { await runScript(controller); }, onConsoleMessage: (controller, consoleMessage) { debugPrint('WebView Console: ${consoleMessage.message}'); }, ); await view.run(); final ticker = Timer.periodic(const Duration(seconds: 3), (t) async { if (completer.isCompleted) { t.cancel(); return; } final controller = view?.webViewController; if (controller != null) { await runScript(controller, force: t.tick >= 5); } }); try { return await completer.future.timeout(timeout); } finally { ticker.cancel(); await importCookies(); await view.dispose(); } } String wrapIifeAsFunctionBody(String iife) { return ''' const __out = await $iife; if (__out == null) return ''; if (typeof __out === 'string') return __out; try { return JSON.stringify(__out); } catch (e) { return String(__out); } '''; } abstract class WebViewBaseProvider extends BaseProvider { @override bool get isWebView => true; @override Duration get requestTimeout => const Duration(seconds: 90); /// The origin to load so Cloudflare / same-origin fetch can run. String get baseUrl; /// The specific API endpoint to call via JS `fetch`. String get apiEndpoint; @override Stream createAsyncGenerator( String model, List messages, { bool stream = false, }) async* { final raw = await runOriginWebView( originUrl: baseUrl, functionBody: wrapIifeAsFunctionBody( buildJsFetchCode(model, messages, stream), ), timeout: requestTimeout, ); yield parseResponse(raw); } /// Builds an IIFE that returns the API body (string or JSON). String buildJsFetchCode(String model, List messages, bool stream); /// Parses the raw string returned by the JS code into the final AI answer. String parseResponse(String rawResponse); }