import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:miaoji_zhang/shared/api/backend_identity.dart'; import 'package:miaoji_zhang/shared/services/session_store.dart'; class ApiClient { ApiClient._(); static final ApiClient instance = ApiClient._(); static const _storage = FlutterSecureStorage(); static const _legacyTokenKey = 'auth_token'; static final _tokenKey = 'auth_token_${BackendIdentity.scope}'; static final sessionExpired = ValueNotifier(0); static bool _handlingUnauthorized = false; static const String baseUrl = BackendIdentity.baseUrl; static bool get isInternalBuild => BackendIdentity.internalBuild; late final Dio dio = _createDio(); Dio _createDio() { final client = Dio( BaseOptions( baseUrl: baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 120), ), ) ..interceptors.add( InterceptorsWrapper( onRequest: (options, handler) async { final token = await _storage.read(key: _tokenKey); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } handler.next(options); }, onError: (error, handler) async { final unauthorized = error.response?.statusCode == 401; final data = error.response?.data; final aiDenied = error.response?.statusCode == 403 && data is Map && data['code'] == 'AI_PERMISSION_DENIED'; if (aiDenied) { await SessionStore.instance.setAiEnabled(false); } final isAuthRequest = error.requestOptions.path.startsWith( '/api/auth/', ); if (unauthorized && !isAuthRequest && !_handlingUnauthorized) { _handlingUnauthorized = true; await SessionStore.instance.markNeedsReauth(); sessionExpired.value++; Future.delayed( const Duration(seconds: 1), () => _handlingUnauthorized = false, ); } handler.next(error); }, ), ); return client; } Future saveToken(String token) async { await _storage.write(key: _tokenKey, value: token); await _storage.delete(key: _legacyTokenKey); } Future readToken() => _storage.read(key: _tokenKey); Future clearToken() async { await _storage.delete(key: _tokenKey); await _storage.delete(key: _legacyTokenKey); } } bool isConnectivityError(Object error) => error is DioException && (error.type == DioExceptionType.connectionTimeout || error.type == DioExceptionType.connectionError || error.type == DioExceptionType.receiveTimeout || error.type == DioExceptionType.sendTimeout); String apiErrorMessage(Object e) { if (e is OnlineFeatureRequiredException) return e.message; if (e is StateError) return e.message; if (e is DioException) { final data = e.response?.data; if (data is Map && data['message'] != null) return data['message'] as String; if (e.type == DioExceptionType.connectionTimeout || e.type == DioExceptionType.connectionError || e.type == DioExceptionType.receiveTimeout) { return '无法连接服务器,请检查网络'; } } return '出错了,请稍后再试'; }