Files
Alma Alexyz fbf04d90db 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.
2026-09-01 22:05:38 +05:00

214 lines
6.4 KiB
Dart

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 'webview_base_provider.dart';
/// Blackbox web chat. Tries REST after scraping a `validated` token, then WebView.
class Blackbox extends WebViewBaseProvider {
@override
String get name => 'Blackbox';
@override
bool get working => true;
@override
List<String> get models => ['blackboxai', 'gpt-4o'];
@override
String get defaultModel => 'blackboxai';
@override
String get baseUrl => 'https://www.blackbox.ai/';
@override
String get apiEndpoint => 'https://www.blackbox.ai/api/chat';
static const _uuid = Uuid();
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
try {
await for (final chunk in _tryRest(messages, stream: stream)) {
yield chunk;
}
return;
} catch (_) {}
await for (final chunk in super.createAsyncGenerator(model, messages, stream: stream)) {
yield chunk;
}
}
Stream<String> _tryRest(List<Message> messages, {required bool stream}) async* {
final validated = await _scrapeValidated();
final payload = {
'messages': [
for (final m in messages)
{
'id': _uuid.v4(),
'content': m.content,
'role': m.role.name,
}
],
'id': _uuid.v4(),
'previewToken': null,
'userId': null,
'codeModelMode': true,
'agentMode': {},
'trendingAgentMode': {},
'isMicMode': false,
'maxTokens': 1024,
'isChromeExt': false,
'clickedAnswer2': false,
'clickedAnswer3': false,
'clickedForceWebSearch': false,
'visitFromDelta': false,
'mobileClient': false,
'userSelectedModel': null,
if (validated != null) 'validated': validated,
'imageGenerationMode': false,
'webSearchMode': false,
};
final urls = [
'https://www.blackbox.ai/api/chat',
'https://api.blackbox.ai/api/chat',
];
Object? lastError;
for (final url in urls) {
try {
final req = http.Request('POST', Uri.parse(url));
req.headers.addAll(browserHeaders(
origin: 'https://www.blackbox.ai',
referer: 'https://www.blackbox.ai/',
contentType: 'application/json',
));
req.body = jsonEncode(payload);
final res = await g4fHttp.send(req).timeout(const Duration(seconds: 20));
if (res.statusCode != 200) {
lastError = 'HTTP ${res.statusCode}';
continue;
}
final collected = StringBuffer();
await for (final chunk in res.stream.transform(utf8.decoder)) {
if (chunk.isEmpty) continue;
// Blackbox sometimes streams raw text, sometimes SSE.
if (chunk.trimLeft().startsWith('data:')) {
for (final piece in extractAllSseContent(chunk)) {
collected.write(piece);
if (stream) yield piece;
}
} else {
collected.write(chunk);
if (stream) yield chunk;
}
}
final text = collected.toString().trim();
if (!looksLikeModelOutput(text)) {
lastError = 'unusable body';
continue;
}
if (!stream) yield text;
return;
} catch (e) {
lastError = e;
}
}
throw Exception('Blackbox REST failed: $lastError');
}
Future<String?> _scrapeValidated() async {
try {
final page = await g4fHttp.get(
Uri.parse('https://www.blackbox.ai/'),
headers: browserHeaders(referer: 'https://www.blackbox.ai/'),
).timeout(const Duration(seconds: 12));
if (page.statusCode != 200) return null;
final chunks = RegExp(r'/_next/static/[^"\x27]+\.js').allMatches(page.body);
final uuidRe = RegExp(
r'[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}',
caseSensitive: false,
);
for (final m in chunks.take(20)) {
final src = m.group(0)!;
final js = await g4fHttp.get(
Uri.parse('https://www.blackbox.ai$src'),
headers: browserHeaders(),
).timeout(const Duration(seconds: 8));
if (js.statusCode != 200) continue;
if (!js.body.contains('validated') && !js.body.contains('api/chat')) {
continue;
}
final match = uuidRe.firstMatch(js.body);
if (match != null) return match.group(0);
}
} catch (_) {}
return null;
}
@override
String buildJsFetchCode(String model, List<Message> messages, bool stream) {
final messagesJson = jsonEncode([
for (final m in messages)
{'id': _uuid.v4(), 'content': m.content, 'role': m.role.name}
]);
return '''
(async function() {
const payload = {
messages: $messagesJson,
id: crypto.randomUUID(),
previewToken: null,
userId: null,
codeModelMode: true,
agentMode: {},
trendingAgentMode: {},
isMicMode: false,
maxTokens: 1024
};
const urls = ['/api/chat', 'https://www.blackbox.ai/api/chat'];
for (const url of urls) {
try {
const resp = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
if (resp.status === 200) {
return await resp.text();
}
} catch (e) {}
}
return JSON.stringify({error: 'Blackbox chat endpoint not available'});
})();
''';
}
@override
String parseResponse(String rawResponse) {
if (looksLikeModelOutput(rawResponse) && !rawResponse.trim().startsWith('{')) {
return rawResponse.trim();
}
try {
final json = jsonDecode(rawResponse);
if (json is Map && json['error'] != null) {
throw Exception(json['error']);
}
} catch (e) {
if (e.toString().contains('Blackbox') || e.toString().contains('error')) {
rethrow;
}
}
final sse = extractAllSseContent(rawResponse).join();
if (looksLikeModelOutput(sse)) return sse;
if (looksLikeModelOutput(rawResponse)) return rawResponse.trim();
throw Exception('Failed to parse Blackbox: ${rawResponse.substring(0, rawResponse.length.clamp(0, 180))}');
}
}