Replace the OpenRouter HTTP client with a DeepSeek client: - data/openrouter/ -> data/deepseek/; OpenRouterClient -> DeepSeekClient (+ DeepSeekCompletion / DeepSeek*Exception) - base URL api.deepseek.com, drop OpenRouter-only headers - DeepSeek has no strict json_schema: use response_format json_object and rely on the prompt for shape (ai_schema.dart removed) - secure-storage key openrouter_api_key -> deepseek_api_key - default model deepseek/deepseek-v4-flash -> deepseek-chat - l10n strings, test dart-defines (DEEPSEEK_API_KEY/_TEST_MODEL), docs Also bundles in-progress findBodyIgnoreRule (body-level ignore rules applied before the AI call) already present in the working tree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
51 lines
2.1 KiB
Dart
51 lines
2.1 KiB
Dart
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
|
|
import '../data/deepseek/deepseek_client.dart';
|
|
import '../data/parser/ai_parser.dart';
|
|
import '../data/secure/ai_key_store.dart';
|
|
|
|
part 'ai_providers.g.dart';
|
|
|
|
/// Защищённое хранилище DeepSeek API key.
|
|
@Riverpod(keepAlive: true)
|
|
AiKeyStore aiKeyStore(Ref ref) =>
|
|
const AiKeyStore(FlutterSecureStorage());
|
|
|
|
/// Общий HTTP-клиент (закрывается при dispose контейнера).
|
|
@Riverpod(keepAlive: true)
|
|
http.Client httpClient(Ref ref) {
|
|
final client = http.Client();
|
|
ref.onDispose(client.close);
|
|
return client;
|
|
}
|
|
|
|
/// AI-парсер, собранный на текущем API key. `null`, если ключ не задан —
|
|
/// воркер в этом случае не зовёт AI. В тестах провайдер переопределяется.
|
|
@Riverpod(keepAlive: true)
|
|
Future<AiParser?> aiParser(Ref ref) async {
|
|
final key = await ref.watch(aiKeyStoreProvider).getApiKey();
|
|
if (key == null || key.isEmpty) return null;
|
|
final client = DeepSeekClient(
|
|
client: ref.watch(httpClientProvider),
|
|
apiKey: key,
|
|
);
|
|
return AiParser(client);
|
|
}
|
|
|
|
/// Поток «есть сеть» — воркер ретраит `pending_ai` при восстановлении (§7).
|
|
///
|
|
/// `onConnectivityChanged` не реплеит текущее состояние при подписке, поэтому
|
|
/// первым значением отдаём результат явной проверки — иначе `.value` остаётся
|
|
/// `null` (и offline-ветка воркера ошибочно считает, что сеть есть).
|
|
@riverpod
|
|
Stream<bool> isOnline(Ref ref) async* {
|
|
final connectivity = Connectivity();
|
|
bool online(List<ConnectivityResult> results) =>
|
|
results.any((r) => r != ConnectivityResult.none);
|
|
yield online(await connectivity.checkConnectivity());
|
|
yield* connectivity.onConnectivityChanged.map(online);
|
|
}
|