fbf04d90db
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.
214 lines
6.4 KiB
Dart
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))}');
|
|
}
|
|
}
|