Files
Alexyz 7f91366201 feat: Random.org dice and daily word matrix, interpreter voices, curated lexicon
Wire cleromancy rolls and the once-per-UTC-day 000-999 word shuffle to RANDOM.ORG JSON-RPC, with a local CSPRNG fallback. Add settings for interpreter personas and a cleaner default oracle prompt. Replace proper names and bureaucratic filler in the 1000-word lexicon with sacred symbols.
2026-09-08 13:46:11 +05:00

184 lines
6.1 KiB
Dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../http_client.dart';
import '../http_util.dart';
import '../models.dart';
import '../sse.dart';
import 'webview_base_provider.dart';
/// Duck.ai / DuckDuckGo chat. REST first; WebView executes the JS anti-bot hash.
class DuckDuckGo extends WebViewBaseProvider {
@override
String get name => 'DuckDuckGo';
@override
bool get working => true;
@override
List<String> get models => [
'gpt-5.6-luna',
'gpt-5.4-mini',
'claude-haiku-4-5',
'mistralai/Mistral-Small-24B-Instruct-2501',
'meta-llama/Llama-4-Scout',
];
@override
String get defaultModel => 'gpt-5.6-luna';
@override
String get baseUrl => 'https://duckduckgo.com/';
@override
String get apiEndpoint => 'https://duckduckgo.com/duckchat/v1/chat';
static const _statusUrl = 'https://duckduckgo.com/duckchat/v1/status';
static const _chatUrl = 'https://duckduckgo.com/duckchat/v1/chat';
@override
Stream<String> createAsyncGenerator(
String model,
List<Message> messages, {
bool stream = false,
}) async* {
try {
await for (final chunk in _tryRest(model, messages, stream: stream)) {
yield chunk;
}
return;
} catch (_) {
// 418 challenge — fall through to a real browser context.
}
await for (final chunk in super.createAsyncGenerator(model, messages, stream: stream)) {
yield chunk;
}
}
Stream<String> _tryRest(
String model,
List<Message> messages, {
required bool stream,
}) async* {
final statusReq = http.Request('GET', Uri.parse(_statusUrl));
statusReq.headers.addAll(browserHeaders(
origin: 'https://duckduckgo.com',
referer: 'https://duckduckgo.com/',
accept: 'application/json',
extra: {'x-vqd-accept': '1'},
));
final statusRes = await g4fHttp.send(statusReq).timeout(const Duration(seconds: 12));
final vqd4 = statusRes.headers['x-vqd-4'];
final hash1 = statusRes.headers['x-vqd-hash-1'];
await statusRes.stream.drain();
if (vqd4 == null || vqd4.isEmpty) {
throw Exception('DuckDuckGo: no x-vqd-4 (challenge required)');
}
final chatReq = http.Request('POST', Uri.parse(_chatUrl));
chatReq.headers.addAll(browserHeaders(
origin: 'https://duckduckgo.com',
referer: 'https://duckduckgo.com/',
contentType: 'application/json',
accept: 'text/event-stream',
extra: {
'x-vqd-4': vqd4,
if (hash1 != null && hash1.isNotEmpty) 'x-vqd-hash-1': hash1,
},
));
chatReq.body = jsonEncode({
'model': models.contains(model) ? model : defaultModel,
'messages': toOpenAiMessages(messages),
'canUseTools': false,
'canUseApproxLocation': false,
});
final chatRes = await g4fHttp.send(chatReq).timeout(const Duration(seconds: 25));
if (chatRes.statusCode != 200) {
throw Exception('DuckDuckGo chat ${chatRes.statusCode}');
}
final collected = StringBuffer();
await for (final line in splitSseLines(chatRes.stream.transform(utf8.decoder))) {
final trimmed = line.trim();
if (trimmed.isEmpty || !trimmed.startsWith('data:')) continue;
final content = extractSseContent(trimmed.substring(5).trim());
if (content != null && content.isNotEmpty) {
collected.write(content);
if (stream) yield content;
}
}
final text = collected.toString();
if (!looksLikeModelOutput(text)) {
throw Exception('DuckDuckGo empty/challenge response');
}
if (!stream) yield text;
}
@override
String buildJsFetchCode(String model, List<Message> messages, bool stream) {
final messagesJson = jsonEncode(toOpenAiMessages(messages));
final modelJson = jsonEncode(models.contains(model) ? model : defaultModel);
return '''
(async function() {
const status = await fetch('https://duckduckgo.com/duckchat/v1/status', {
headers: { 'x-vqd-accept': '1', 'Accept': 'application/json' }
});
let vqd4 = status.headers.get('x-vqd-4') || '';
let hash1 = status.headers.get('x-vqd-hash-1') || '';
if (hash1) {
try {
let src = hash1;
try { src = atob(hash1); } catch (e) {}
if (src.includes('function') || src.trim().startsWith('(')) {
const result = await (0, eval)(src);
const value = (typeof result === 'function') ? await result() : result;
hash1 = btoa(JSON.stringify(value));
}
} catch (e) {
return JSON.stringify({error: 'hash failed: ' + e});
}
}
const headers = {
'Content-Type': 'application/json',
'Accept': 'text/event-stream'
};
if (vqd4) headers['x-vqd-4'] = vqd4;
if (hash1) headers['x-vqd-hash-1'] = hash1;
const resp = await fetch('https://duckduckgo.com/duckchat/v1/chat', {
method: 'POST',
headers,
body: JSON.stringify({
model: $modelJson,
messages: $messagesJson,
canUseTools: false,
canUseApproxLocation: false
})
});
const body = await resp.text();
return JSON.stringify({status: resp.status, body: body});
})();
''';
}
@override
String parseResponse(String rawResponse) {
try {
final json = jsonDecode(rawResponse);
if (json is Map && json['error'] != null) {
throw Exception(json['error']);
}
final body = json is Map ? (json['body']?.toString() ?? rawResponse) : rawResponse;
final text = extractAllSseContent(body).join();
if (!looksLikeModelOutput(text)) {
throw Exception('DuckDuckGo WebView unusable: ${body.substring(0, body.length.clamp(0, 180))}');
}
return text;
} catch (e) {
if (e.toString().contains('DuckDuckGo')) rethrow;
final text = extractAllSseContent(rawResponse).join();
if (looksLikeModelOutput(text)) return text;
throw Exception('Failed to parse DuckDuckGo: $e');
}
}
}