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,75 @@
|
||||
const kBrowserUserAgent =
|
||||
'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36';
|
||||
|
||||
const kDesktopUserAgent =
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
||||
|
||||
Map<String, String> browserHeaders({
|
||||
String? origin,
|
||||
String? referer,
|
||||
String? accept,
|
||||
String? contentType,
|
||||
bool desktop = false,
|
||||
Map<String, String>? extra,
|
||||
}) {
|
||||
return {
|
||||
'User-Agent': desktop ? kDesktopUserAgent : kBrowserUserAgent,
|
||||
'Accept': accept ?? '*/*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
if (origin != null) 'Origin': origin,
|
||||
if (referer != null) 'Referer': referer,
|
||||
if (contentType != null) 'Content-Type': contentType,
|
||||
...?extra,
|
||||
};
|
||||
}
|
||||
|
||||
/// True when a provider dumped an error page / WAF / paywall instead of model text.
|
||||
bool looksLikeGarbage(String text) {
|
||||
final t = text.trimLeft();
|
||||
if (t.isEmpty) return true;
|
||||
final lower = t.toLowerCase();
|
||||
if (t.startsWith('<!DOCTYPE') || t.startsWith('<html') || t.startsWith('<HTML')) {
|
||||
return true;
|
||||
}
|
||||
if (lower.contains('just a moment') ||
|
||||
lower.contains('cf-browser-verification') ||
|
||||
lower.contains('attention required') ||
|
||||
lower.contains('payment required') ||
|
||||
lower.contains('frontend_captcha') ||
|
||||
lower.contains('err_challenge') ||
|
||||
lower.contains('error code:') ||
|
||||
lower.contains('missing api key') ||
|
||||
lower.contains('invalid api key') ||
|
||||
lower.contains('you have to be logged in') ||
|
||||
lower.contains('no cake credits') ||
|
||||
lower.contains('insufficient_credits')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool looksLikeModelOutput(String text) {
|
||||
final trimmed = text.trim();
|
||||
if (trimmed.length < 8) return false;
|
||||
final lower = trimmed.toLowerCase();
|
||||
if (lower == ': ping' || lower == 'ping' || lower == 'pong' || lower == '{}') {
|
||||
return false;
|
||||
}
|
||||
if (RegExp(r'^[\{\}\[\]":\s]+$').hasMatch(trimmed)) return false;
|
||||
return !looksLikeGarbage(trimmed);
|
||||
}
|
||||
|
||||
String? cookieHeaderFromSetCookie(String? setCookie, {String? name}) {
|
||||
if (setCookie == null || setCookie.isEmpty) return null;
|
||||
// http package may join multiple Set-Cookie values with ", ".
|
||||
final parts = setCookie.split(RegExp(r',(?=\s*[^;=]+=)'));
|
||||
final pairs = <String>[];
|
||||
for (final part in parts) {
|
||||
final nv = part.split(';').first.trim();
|
||||
if (nv.isEmpty || !nv.contains('=')) continue;
|
||||
if (name != null && !nv.startsWith('$name=')) continue;
|
||||
pairs.add(nv);
|
||||
}
|
||||
if (pairs.isEmpty) return null;
|
||||
return pairs.join('; ');
|
||||
}
|
||||
Reference in New Issue
Block a user