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,98 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../http_client.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../http_util.dart';
|
||||
import '../models.dart';
|
||||
import '../sse.dart';
|
||||
import 'base_provider.dart';
|
||||
|
||||
/// Port of g4f.Provider.Liaobots — guest auth code + chat.
|
||||
class Liaobots extends BaseProvider {
|
||||
@override
|
||||
String get name => 'Liaobots';
|
||||
|
||||
@override
|
||||
bool get working => true;
|
||||
|
||||
@override
|
||||
List<String> get models => ['gpt-3.5-turbo', 'gpt-4'];
|
||||
|
||||
@override
|
||||
String get defaultModel => 'gpt-3.5-turbo';
|
||||
|
||||
static const _hosts = [
|
||||
'https://liaobots.work',
|
||||
'https://liaobots.site',
|
||||
'https://liaobots.com',
|
||||
];
|
||||
|
||||
@override
|
||||
Stream<String> createAsyncGenerator(
|
||||
String model,
|
||||
List<Message> messages, {
|
||||
bool stream = false,
|
||||
}) async* {
|
||||
Object? last;
|
||||
for (final host in _hosts) {
|
||||
try {
|
||||
yield await _chat(host, model, messages);
|
||||
return;
|
||||
} catch (e) {
|
||||
last = e;
|
||||
}
|
||||
}
|
||||
throw Exception('Liaobots failed: $last');
|
||||
}
|
||||
|
||||
Future<String> _chat(String host, String model, List<Message> messages) async {
|
||||
final headers = browserHeaders(
|
||||
origin: host,
|
||||
referer: '$host/',
|
||||
contentType: 'application/json',
|
||||
);
|
||||
final login = await g4fHttp
|
||||
.post(
|
||||
Uri.parse('$host/api/user'),
|
||||
headers: headers,
|
||||
body: jsonEncode({'authcode': const Uuid().v4()}),
|
||||
)
|
||||
.timeout(const Duration(seconds: 12));
|
||||
String? auth;
|
||||
if (login.statusCode == 200) {
|
||||
try {
|
||||
final data = jsonDecode(login.body);
|
||||
auth = (data is Map ? (data['authCode'] ?? data['authcode']) : null)
|
||||
?.toString();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final req = http.Request('POST', Uri.parse('$host/api/chat'));
|
||||
req.headers.addAll({
|
||||
...headers,
|
||||
if (auth != null) 'x-auth-code': auth,
|
||||
if (auth != null) 'authorization': auth,
|
||||
});
|
||||
req.body = jsonEncode({
|
||||
'conversationId': const Uuid().v4(),
|
||||
'model': {
|
||||
'id': models.contains(model) ? model : defaultModel,
|
||||
'name': model,
|
||||
'maxLength': 24000,
|
||||
'tokenLimit': 8000,
|
||||
},
|
||||
'messages': toOpenAiMessages(messages),
|
||||
'key': '',
|
||||
'prompt': extractSystemMessage(messages),
|
||||
});
|
||||
final res = await g4fHttp.send(req).timeout(requestTimeout);
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception('Liaobots ${res.statusCode} @ $host');
|
||||
}
|
||||
final raw = utf8.decode(await res.stream.toBytes().timeout(requestTimeout));
|
||||
final sse = extractAllSseContent(raw).join();
|
||||
final text = looksLikeModelOutput(sse) ? sse : raw.trim();
|
||||
if (!looksLikeModelOutput(text)) throw Exception('Liaobots empty @ $host');
|
||||
return text;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user