feat: embed a Flutter G4F client with Cronet, cookie jar, and origin WebView
Port the Oracle off the Python g4f container onto on-device providers: Chromium HTTP via Cronet, a shared cookie session, IterList retry, hosted OpenAI-compat aggregators, and same-origin WebView fallbacks instead of CORS fetch.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cronet_http/cronet_http.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'cookie_jar.dart';
|
||||
import 'http_util.dart';
|
||||
|
||||
/// Shared HTTP stack for every G4F provider.
|
||||
///
|
||||
/// Python g4f uses `curl_cffi(impersonate="chrome")` plus a session cookie
|
||||
/// jar. On Android the closest TLS equivalent is Cronet; cookies are kept
|
||||
/// in [g4fCookies] because Cronet does not expose a Dart cookie store.
|
||||
http.Client createG4fClient() {
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
final engine = CronetEngine.build(
|
||||
cacheMode: CacheMode.memory,
|
||||
cacheMaxSize: 4 * 1024 * 1024,
|
||||
userAgent: kDesktopUserAgent,
|
||||
enableHttp2: true,
|
||||
enableBrotli: true,
|
||||
enableQuic: true,
|
||||
);
|
||||
return CronetClient.fromCronetEngine(engine);
|
||||
}
|
||||
return http.Client();
|
||||
}
|
||||
|
||||
final G4fCookieJar g4fCookies = G4fCookieJar();
|
||||
|
||||
final http.Client g4fHttp = CookieClient(createG4fClient(), g4fCookies);
|
||||
|
||||
class CookieClient extends http.BaseClient {
|
||||
CookieClient(this._inner, this.jar);
|
||||
|
||||
final http.Client _inner;
|
||||
final G4fCookieJar jar;
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
final fromJar = jar.headerFor(request.url);
|
||||
if (fromJar.isNotEmpty) {
|
||||
final existing = request.headers['cookie'];
|
||||
if (existing == null || existing.isEmpty) {
|
||||
request.headers['cookie'] = fromJar;
|
||||
} else {
|
||||
request.headers['cookie'] = '$existing; $fromJar';
|
||||
}
|
||||
}
|
||||
final response = await _inner.send(request);
|
||||
jar.absorb(request.url, response.headers);
|
||||
return response;
|
||||
}
|
||||
|
||||
@override
|
||||
void close() => _inner.close();
|
||||
}
|
||||
|
||||
final Set<String> _warmedOrigins = {};
|
||||
|
||||
/// GET the provider origin once so Cloudflare/session cookies land in [g4fCookies].
|
||||
Future<void> warmupOrigin(String url) async {
|
||||
if (url.isEmpty) return;
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null || !uri.hasScheme || uri.host.isEmpty) return;
|
||||
final origin = uri.origin;
|
||||
if (_warmedOrigins.contains(origin)) return;
|
||||
_warmedOrigins.add(origin);
|
||||
try {
|
||||
await g4fHttp
|
||||
.get(
|
||||
uri,
|
||||
headers: browserHeaders(
|
||||
desktop: true,
|
||||
accept:
|
||||
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
referer: '$origin/',
|
||||
),
|
||||
)
|
||||
.timeout(const Duration(seconds: 12));
|
||||
} catch (_) {}
|
||||
}
|
||||
Reference in New Issue
Block a user