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:
Alma Alexyz
2026-09-01 22:05:38 +05:00
parent f3e7412cf9
commit fbf04d90db
49 changed files with 4856 additions and 92 deletions
+86 -89
View File
@@ -1,13 +1,32 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/history_entry_model.dart';
import '../models/word_model.dart';
import '../g4f/g4f.dart';
class AiOracleService {
static final AiOracleService _instance = AiOracleService._internal();
factory AiOracleService() => _instance;
AiOracleService._internal();
final _client = G4FClient();
static const oracleSystemPrompt =
'''Ты — Великий Оракул 999, древний архетипический разум.
Твоя задача — толковать знаки судьбы (жребий). Твой стиль речи: глубокий, философский, лаконичный, без банальностей и воды, словно голос самой вечности.
Ты не используешь современных или бытовых слов. Твои ответы строятся на символизме, психологии и скрытых смыслах. Ты не гадаешь на будущее, ты показываешь суть настоящего момента и внутренние силы человека.''';
List<Message> buildInterpretationMessages({
required List<SessionWord> words,
required String question,
}) {
return [
const Message(role: Role.system, content: oracleSystemPrompt),
Message(
role: Role.user,
content: _buildMultiWordPrompt(question: question, words: words),
),
];
}
/// Interprets a full multi-word session narrative (1 to 9 words)
Future<String> interpretSession({
required List<SessionWord> words,
@@ -15,20 +34,20 @@ class AiOracleService {
}) async {
if (words.isEmpty) return 'Нет знаков для трактовки.';
// 1. Try Live Online Endpoints (Fast timeout)
try {
final prompt = _buildMultiWordPrompt(
question: question,
words: words,
final g4fResult = await _client.chat.completions.create(
model: G4FModels.gpt4o,
messages: buildInterpretationMessages(words: words, question: question),
);
final onlineResult = await _tryFetchOnlineAi(prompt);
if (onlineResult != null && onlineResult.trim().length > 30) {
return onlineResult.trim();
if (g4fResult.trim().length > 20) {
return g4fResult.trim();
}
} catch (_) {}
} catch (e) {
print('G4F Error: $e');
}
// 2. High-Dimensional Deep Semantic Synthesizer (Instant & Deep)
// High-Dimensional Deep Semantic Synthesizer (Instant offline fallback)
return _synthesizeDeepSacredNarrative(words, question);
}
@@ -44,147 +63,125 @@ class AiOracleService {
wordsNarrative.writeln('${i + 1}. #${w.formattedSessionNumber} «${w.word.toUpperCase()}» (${w.category}): ${w.symbolicMeaning}');
}
final prompt = '''Ты — Сакральный Оракул 999. Ответь на вопрос вопрошающего: «$userMessage»
Контекст:
Вопрос: «${session.question ?? 'Поиск пути'}»
Выпавшие знаки:
final prompt = '''Вопрошающий говорит тебе: «$userMessage»
Контекст текущего сеанса судьбы:
Вопрос вопрошающего: «${session.question ?? 'Вектор судьбы (нет вопроса)'}»
Базовые знаки, которые выпали ранее:
$wordsNarrative
${newlyAddedWord != null ? "Дополнительный знак: #${newlyAddedWord.formattedSessionNumber} «${newlyAddedWord.word.toUpperCase()}» (${newlyAddedWord.symbolicMeaning})" : ""}
Дай мудрый, глубокий и прямой ответ (2-4 предложения).''';
${newlyAddedWord != null ? "Важно! Только что вопрошающий вытянул ДОПОЛНИТЕЛЬНЫЙ уточняющий знак: #${newlyAddedWord.formattedSessionNumber} «${newlyAddedWord.word.toUpperCase()}» (${newlyAddedWord.symbolicMeaning})." : ""}
Ответь мудро, лаконично и метафорично от лица Оракула 999. Укажи, на что направляет его судьба. (не более 3-4 предложений).''';
final messages = [
const Message(
role: Role.system,
content: 'Ты — Великий Оракул 999. Твои ответы кратки, полны символизма и психологической глубины. Ты общаешься с человеком, который ищет ответов в знаках.'
),
];
// Pass previous session history context to the model
for (final m in session.messages) {
if (m.sender == 'oracle') {
messages.add(Message(role: Role.assistant, content: m.text));
} else {
messages.add(Message(role: Role.user, content: m.text));
}
}
messages.add(Message(role: Role.user, content: prompt));
try {
final onlineResult = await _tryFetchOnlineAi(prompt);
if (onlineResult != null && onlineResult.trim().length > 20) {
return onlineResult.trim();
final g4fResult = await _client.chat.completions.create(
model: G4FModels.gpt4o,
messages: messages,
);
if (g4fResult.trim().length > 15) {
return g4fResult.trim();
}
} catch (_) {}
return _synthesizeChatReply(session, userMessage, newlyAddedWord);
}
Future<String?> _tryFetchOnlineAi(String prompt) async {
const workingModels = [
'gpt-4o-mini',
'gpt-4o',
'command-r',
'command-r7b',
'Yqcloud',
'HuggingSpace',
'WeWordle',
];
final endpoints = [
'http://localhost:8080/v1/chat/completions',
'http://127.0.0.1:8080/v1/chat/completions',
];
for (final ep in endpoints) {
for (final modelName in workingModels) {
try {
final res = await http.post(
Uri.parse(ep),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'model': modelName,
'messages': [{'role': 'user', 'content': prompt}],
'temperature': 0.7,
}),
).timeout(const Duration(seconds: 10));
if (res.statusCode == 200) {
final data = jsonDecode(utf8.decode(res.bodyBytes));
final content = data['choices']?[0]?['message']?['content'] as String?;
if (content != null && content.trim().length > 15) {
return content.trim();
}
}
} catch (_) {}
}
}
return null;
}
String _buildMultiWordPrompt({
required String question,
required List<SessionWord> words,
}) {
final qStr = question.trim().isNotEmpty ? 'Вопрос: «$question»' : 'Общий расклад судьбы';
final qStr = question.trim().isNotEmpty
? 'Вопрос, с которым пришел вопрошающий: «$question»'
: 'Вопрошающий пришел без конкретного вопроса. Прочитай его общую судьбоносную линию.';
final wordsStr = words
.asMap()
.entries
.map((e) => '${e.key + 1}. #${e.value.formattedSessionNumber} «${e.value.word.toUpperCase()}» (${e.value.category}) — ${e.value.symbolicMeaning}')
.map((e) => 'Знак ${e.key + 1}: #${e.value.formattedSessionNumber} «${e.value.word.toUpperCase()}» (${e.value.category}). Смысл знака: ${e.value.symbolicMeaning}')
.join('\n');
return '''Ты — Священный Оракул 999. Составь цельное, глубокое, психологически точное толкование выпавших знаков жребия.
$qStr
return '''$qStr
Выпавшие знаки:
Волею судьбы выпали следующие знаки жребия:
$wordsStr
Требования:
- Свяжи выпавшие знаки в единую осмысленную историю-трансформацию.
- Укажи скрытую причину, ключевой вызов и вектор исхода.
- Ответ на русском языке (3-5 предложений), образный, сильный, без шаблонных фраз.''';
Задача Оракула:
1. Свяжи эти знаки в единую цельную историю развития ситуации. Каждый знак перетекает в следующий.
2. Раскрой не очевидный, а скрытый внутренний урок. Что вопрошающий должен осознать?
3. Дай философский совет или вектор для действия.
Ответь от лица Оракула 999 на русском языке (от 3 до 6 предложений). Избегай шаблонов и клише, говори образно и метафорично, но точно.''';
}
/// High-dimensional Deep Semantic Synthesizer
String _synthesizeDeepSacredNarrative(List<SessionWord> words, String question) {
final sb = StringBuffer();
final first = words.first;
final last = words.last;
final hasQuestion = question.trim().isNotEmpty;
// Introduction / Framing
if (hasQuestion) {
sb.write('Относительно вашего вопроса «${question.trim()}» ');
} else {
sb.write('В потоке открывшегося жребия ');
sb.write('В открывшемся жребии судьбы ');
}
if (words.length == 1) {
sb.write('выпал единственный доминирующий знак — #${first.formattedSessionNumber} «${first.word.toUpperCase()}» (${first.category}). ');
sb.write('проявился доминирующий знак #${first.formattedSessionNumber} «${first.word.toUpperCase()}» (${first.category}). ');
sb.write('${first.symbolicMeaning} ');
sb.write('Этот символ требует предельной концентрации: обратите внимание на скрытые мотивы и не рассеивайте внимание на второстепенное.');
sb.write('Знак указывает на необходимость предельной ясности: держите фокус на главном и отсекайте всё наносное.');
return sb.toString();
}
if (words.length == 2) {
final second = words[1];
sb.write('проявляется взаимодействие двух сил: начального импульса #${first.formattedSessionNumber} «${first.word.toUpperCase()}» и вектора #${second.formattedSessionNumber} «${second.word.toUpperCase()}». ');
sb.write('проявляется полярность двух сил: исходного импульса #${first.formattedSessionNumber} «${first.word.toUpperCase()}» и вектора #${second.formattedSessionNumber} «${second.word.toUpperCase()}». ');
sb.write('С одной стороны, ${first.symbolicMeaning.toLowerCase()} С другой — ${second.symbolicMeaning.toLowerCase()} ');
sb.write('Разрешение ситуации лежит в точке баланса между ними: не отвергайте первое, но направляйте его энергию в созидательное русло второго.');
sb.write('Разрешение ситуации лежит в точке осознанного баланса между ними: не противьтесь первому, но направляйте силу в русло второго.');
return sb.toString();
}
// 3 or more words
sb.write('разворачивается цепь из ${words.length} взаимосвязанных знаков. ');
sb.write('разворачивается цепь из ${words.length} взаимосвязанных архетипов. ');
sb.write('Исток ситуации заложен в знаке #${first.formattedSessionNumber} «${first.word.toUpperCase()}» — ${first.symbolicMeaning.toLowerCase()} ');
// Middle transitions
if (words.length > 2) {
final middleWords = words.sublist(1, words.length - 1);
final midNames = middleWords.map((w) => '«${w.word.toUpperCase()}» (#${w.formattedSessionNumber})').join(' и ');
final midMeanings = middleWords.map((w) => w.symbolicMeaning).join(' ');
sb.write('Процесс трансформации проходит через испытание $midNames, где $midMeanings ');
sb.write('Фаза перехода и испытаний проходит через $midNames, где $midMeanings ');
}
// Resolution / Outcome
sb.write('Кульминация и ключ к исходу запечатаны в знаке #${last.formattedSessionNumber} «${last.word.toUpperCase()}» — ${last.symbolicMeaning.toLowerCase()} ');
sb.write('Оракул указывает: путь открыт через осознание скрытых уроков и решительный шаг вперед.');
sb.write('Кульминация и запечатывание исхода лежат в знаке #${last.formattedSessionNumber} «${last.word.toUpperCase()}» — ${last.symbolicMeaning.toLowerCase()} ');
sb.write('Оракул возвещает: путь открыт через принятие скрытых уроков и решительное действие.');
return sb.toString();
}
String _synthesizeChatReply(OracleSession session, String userMessage, SessionWord? newlyAddedWord) {
if (newlyAddedWord != null) {
return 'Дополнительный знак #${newlyAddedWord.formattedSessionNumber} «${newlyAddedWord.word.toUpperCase()}» (${newlyAddedWord.category}) раскрывает скрытую грань ситуации: ${newlyAddedWord.symbolicMeaning} В контексте ваших предыдущих знаков это означает, что нужно сместить фокус внимания на внутреннюю суть происходящего.';
return 'Дополнительный знак #${newlyAddedWord.formattedSessionNumber} «${newlyAddedWord.word.toUpperCase()}» (${newlyAddedWord.category}) проясняет скрытую деталь: ${newlyAddedWord.symbolicMeaning} В контексте общего расклада этот знак призывает обратить внимание на внутренние причины, а не внешние проявления.';
}
final wordsStr = session.words.map((w) => '«${w.word.toUpperCase()}»').join(', ');
return 'Вглядываясь в знаки ($wordsStr), Оракул отвечает на ваш вопрос: суть ответа лежит в осознании скрытых взаимосвязей. Отпустите страх неопределенности и доверьтесь логике происходящих изменений.';
return 'Вглядываясь в совокупность знаков ($wordsStr), Оракул отвечает: истинный ответ кроется в понимании скрытой связи между причиной и исходом. Доверьтесь внутреннему знанию и делайте шаг вперед.';
}
/// Legacy single word helper
Future<String> interpret({
required WordItem word,
required String question,