import 'dart:convert'; import 'package:flutter/foundation.dart'; import '../http_client.dart'; import '../http_util.dart'; import '../models.dart'; import 'base_provider.dart'; import 'webview_base_provider.dart'; class PhindAi extends BaseProvider { @override String get name => 'PhindAi'; @override String get label => 'PhindAi'; @override String get url => 'https://phindai.org'; @override bool get working => true; @override bool get supportsStream => false; @override Duration get requestTimeout => const Duration(seconds: 90); @override String get defaultModel => 'deepseek-v3'; @override List get models => ['deepseek-v3', 'deepseek']; @override Stream createAsyncGenerator( String model, List messages, { bool stream = false, }) async* { try { yield await _rest(messages); return; } catch (e) { debugPrint('Phind REST failed, trying origin WebView: $e'); } yield await _viaWebView(messages); } Future _rest(List messages) async { final prompt = messages.last.content; final headers = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', 'User-Agent': kDesktopUserAgent, }; final mainPageResponse = await g4fHttp.get(Uri.parse(url), headers: headers); if (mainPageResponse.statusCode != 200) { throw Exception('Failed to fetch PhindAi main page: ${mainPageResponse.statusCode}'); } final html = mainPageResponse.body; final match = RegExp(r'"nonce":"([a-f0-9]+)"').firstMatch(html) ?? RegExp(r"'nonce':'([a-f0-9]+)'").firstMatch(html); if (match == null) { throw Exception('Failed to extract nonce from PhindAi response'); } final nonce = match.group(1)!; final response = await g4fHttp.post( Uri.parse('$url/wp-admin/admin-ajax.php'), headers: headers, body: { 'action': 'phind_ai_send', 'nonce': nonce, 'message': prompt, }, ); if (response.statusCode != 200) { throw Exception('PhindAi API returned status: ${response.statusCode}'); } final data = jsonDecode(response.body); if (data is! Map || data['success'] != true) { throw Exception('PhindAi API returned success=False: $data'); } final responseText = (data['data'] is Map ? data['data']['response'] : null) ?? ''; final text = responseText.toString().trim(); if (!looksLikeModelOutput(text)) { throw Exception('Phind REST unusable'); } return text; } Future _viaWebView(List messages) async { final promptJson = jsonEncode(messages.last.content); final raw = await runOriginWebView( originUrl: url, timeout: requestTimeout, functionBody: ''' const prompt = $promptJson; const html = document.documentElement.innerHTML; const m = html.match(/"nonce":"([a-f0-9]+)"/) || html.match(/'nonce':'([a-f0-9]+)'/); if (!m) throw new Error('no nonce in page'); const body = new URLSearchParams(); body.set('action', 'phind_ai_send'); body.set('nonce', m[1]); body.set('message', prompt); const r = await fetch('/wp-admin/admin-ajax.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, credentials: 'same-origin', body }); const data = await r.json(); if (!data || data.success !== true) { throw new Error('phind success=false ' + JSON.stringify(data).slice(0, 180)); } return String((data.data && data.data.response) || ''); ''', ); final text = raw.trim(); if (!looksLikeModelOutput(text)) { throw Exception('Phind WebView unusable: ${text.substring(0, text.length.clamp(0, 180))}'); } return text; } }