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,94 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../http_client.dart';
|
||||
import '../http_util.dart';
|
||||
import '../models.dart';
|
||||
import 'base_provider.dart';
|
||||
import 'webview_base_provider.dart';
|
||||
|
||||
class TeachAnything extends BaseProvider {
|
||||
@override
|
||||
String get name => 'TeachAnything';
|
||||
|
||||
@override
|
||||
String get url => 'https://www.teach-anything.com';
|
||||
|
||||
@override
|
||||
bool get working => true;
|
||||
|
||||
@override
|
||||
Duration get requestTimeout => const Duration(seconds: 90);
|
||||
|
||||
@override
|
||||
List<String> get models => ['gemma'];
|
||||
|
||||
@override
|
||||
String get defaultModel => 'gemma';
|
||||
|
||||
@override
|
||||
Stream<String> createAsyncGenerator(
|
||||
String model,
|
||||
List<Message> messages, {
|
||||
bool stream = false,
|
||||
}) async* {
|
||||
try {
|
||||
yield await _rest(messages);
|
||||
return;
|
||||
} catch (e) {
|
||||
debugPrint('TeachAnything REST failed, trying origin WebView: $e');
|
||||
}
|
||||
yield await _viaWebView(messages);
|
||||
}
|
||||
|
||||
Future<String> _rest(List<Message> messages) async {
|
||||
final prompt = formatPrompt(messages);
|
||||
final req = http.Request('POST', Uri.parse('$url/api/generate'));
|
||||
req.headers.addAll({
|
||||
'accept': '*/*',
|
||||
'accept-language': 'en-US,en;q=0.9',
|
||||
'cache-control': 'no-cache',
|
||||
'content-type': 'application/json',
|
||||
'origin': url,
|
||||
'referer': '$url/',
|
||||
'user-agent': kDesktopUserAgent,
|
||||
});
|
||||
req.body = jsonEncode({'prompt': prompt});
|
||||
|
||||
final response = await g4fHttp.send(req).timeout(requestTimeout);
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('$name returned status ${response.statusCode}');
|
||||
}
|
||||
final fullText = utf8.decode(await response.stream.toBytes()).trim();
|
||||
if (!looksLikeModelOutput(fullText)) throw Exception('$name empty');
|
||||
return fullText;
|
||||
}
|
||||
|
||||
Future<String> _viaWebView(List<Message> messages) async {
|
||||
final promptJson = jsonEncode(formatPrompt(messages));
|
||||
final raw = await runOriginWebView(
|
||||
originUrl: url,
|
||||
timeout: requestTimeout,
|
||||
functionBody: '''
|
||||
const prompt = $promptJson;
|
||||
const r = await fetch('/api/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', 'accept': '*/*' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ prompt })
|
||||
});
|
||||
if (!r.ok) throw new Error('status ' + r.status);
|
||||
return await r.text();
|
||||
''',
|
||||
);
|
||||
final text = raw.trim();
|
||||
if (!looksLikeModelOutput(text)) {
|
||||
throw Exception(
|
||||
'TeachAnything WebView unusable: ${text.substring(0, text.length.clamp(0, 180))}',
|
||||
);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user