Initial project import

This commit is contained in:
2026-07-24 23:11:20 +08:00
commit 6396eabb87
372 changed files with 49682 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:miaoji_zhang/shared/services/session_store.dart';
class ApiClient {
ApiClient._();
static final ApiClient instance = ApiClient._();
static const _storage = FlutterSecureStorage();
static const _tokenKey = 'auth_token';
static final sessionExpired = ValueNotifier<int>(0);
static bool _handlingUnauthorized = false;
static const _internalBuild = bool.fromEnvironment('INTERNAL_BUILD');
static const _internalTestBaseUrl = 'https://lt.frp-say.com:38012';
static const _configuredBaseUrl = String.fromEnvironment('API_BASE_URL');
static const _temporaryFrpHost = 'lt.frp-say.com';
static const _temporaryFrpPort = 38012;
static const _temporaryFrpCertificateSha1 =
'509C3210161E72FB9DA5183D3D2152F63871C31F';
static const String baseUrl = _configuredBaseUrl != ''
? _configuredBaseUrl
: _internalBuild
? _internalTestBaseUrl
: 'https://api.invalid';
static bool get isInternalBuild => _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<void>.delayed(
const Duration(seconds: 1),
() => _handlingUnauthorized = false,
);
}
handler.next(error);
},
),
);
final apiUri = Uri.tryParse(baseUrl);
if (_internalBuild &&
apiUri?.scheme == 'https' &&
apiUri?.host == _temporaryFrpHost &&
apiUri?.port == _temporaryFrpPort) {
client.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final httpClient = HttpClient();
httpClient.badCertificateCallback = (certificate, host, port) {
final certificateSha1 = certificate.sha1
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
.join()
.toUpperCase();
return host == _temporaryFrpHost &&
port == _temporaryFrpPort &&
certificateSha1 == _temporaryFrpCertificateSha1;
};
return httpClient;
},
);
}
return client;
}
Future<void> saveToken(String token) =>
_storage.write(key: _tokenKey, value: token);
Future<String?> readToken() => _storage.read(key: _tokenKey);
Future<void> clearToken() => _storage.delete(key: _tokenKey);
}
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 '出错了,请稍后再试';
}