import 'dart:async'; import 'dart:convert'; import 'package:ainoval/config/app_config.dart' hide LogLevel; import 'package:ainoval/models/ai_request_models.dart'; import 'package:ainoval/models/chat_models.dart'; import 'package:ainoval/models/import_status.dart'; import 'package:ainoval/models/model_info.dart'; import 'package:ainoval/models/user_ai_model_config_model.dart'; import 'package:ainoval/services/api_service/base/api_exception.dart'; import 'package:ainoval/services/auth_service.dart'; import 'package:ainoval/utils/logger.dart'; import 'package:dio/dio.dart'; /// API客户端基类 /// /// 负责处理与后端API的基础通信,使用Dio包实现HTTP请求 class ApiClient { ApiClient({Dio? dio, AuthService? authService}) { _authService = authService; _dio = dio ?? _createDio(); } late final Dio _dio; AuthService? _authService; /// 设置AuthService实例(用于处理401错误) void setAuthService(AuthService authService) { _authService = authService; } /// 创建并配置Dio实例 Dio _createDio() { final dio = Dio( BaseOptions( baseUrl: AppConfig.apiBaseUrl, connectTimeout: const Duration(seconds: 30), receiveTimeout: const Duration(minutes: 5), // 🚀 增加到5分钟,支持长时间AI响应 sendTimeout: const Duration(seconds: 30), contentType: 'application/json', responseType: ResponseType.json, ), ); // 添加拦截器 dio.interceptors.add(_createAuthInterceptor()); dio.interceptors.add(_createResponseInterceptor()); dio.interceptors.add(_createLogInterceptor()); return dio; } /// 规范化后端响应数据,避免在 Web 端出现 LegacyJavaScriptObject dynamic _normalizeResponseData(dynamic raw) { if (raw == null) return null; // 如果是 JSON 字符串,先尝试解码 if (raw is String) { final trimmed = raw.trim(); if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) { try { return jsonDecode(trimmed); } catch (_) { return raw; } } return raw; } // 对于 Web 端返回的 JS 对象,使用 JSON 循环编码/解码,转为 Dart 原生 Map/List try { final encoded = jsonEncode(raw); if (encoded.isNotEmpty && (encoded.startsWith('{') || encoded.startsWith('['))) { return jsonDecode(encoded); } } catch (_) { // 忽略,按原样返回 } return raw; } /// 创建认证拦截器 Interceptor _createAuthInterceptor() { return InterceptorsWrapper( onRequest: (options, handler) { final token = AppConfig.authToken; if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } // 添加用户ID头部(后端需要X-User-Id头部) final userId = AppConfig.userId; if (userId != null) { options.headers['X-User-Id'] = userId; } return handler.next(options); }, ); } /// 创建响应拦截器(处理401错误) Interceptor _createResponseInterceptor() { return InterceptorsWrapper( onError: (DioException error, ErrorInterceptorHandler handler) async { // 检查是否为401未授权错误 if (error.response?.statusCode == 401) { AppLogger.w('ApiClient', 'Token过期或无效,执行自动登出'); // 执行登出操作 if (_authService != null) { try { await _authService!.logout(); } catch (e) { AppLogger.e('ApiClient', '自动登出失败', e); } } } return handler.next(error); }, ); } /// 创建日志拦截器 Interceptor _createLogInterceptor() { final currentLogLevel = AppConfig.logLevel; return LogInterceptor( requestBody: currentLogLevel == LogLevel.warning, responseBody: currentLogLevel == LogLevel.warning, error: currentLogLevel == LogLevel.debug || currentLogLevel == LogLevel.error, requestHeader: currentLogLevel == LogLevel.warning, responseHeader: currentLogLevel == LogLevel.warning, ); } /// 基础POST请求方法 Future post(String path, {dynamic data, Options? options}) async { try { // 添加日志记录,显示请求正文 AppLogger.d('ApiClient', '发送POST请求到 $path'); if (data != null) { try { final String jsonData = jsonEncode(data); AppLogger.d('ApiClient', '请求正文: $jsonData'); } catch (e) { AppLogger.d('ApiClient', '请求正文(无法序列化): $data'); } } final response = await _dio.post(path, data: data, options: options); return _normalizeResponseData(response.data); } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', 'post 执行出错,路径: $path', e); throw ApiException(-1, '执行 POST 请求时发生意外错误: ${e.toString()}'); } } /// 基础流式POST请求方法 /// /// 返回原始字节流 Stream> Future>> postStream(String path, {dynamic data, Options? options}) async { try { final response = await _dio.post( path, data: data, options: (options ?? Options()).copyWith(responseType: ResponseType.stream), ); if (response.data != null) { return response.data!.stream; } else { AppLogger.w('ApiClient', 'postStream 收到空的响应数据,路径: $path'); return Stream.error(ApiException(-1, '流式请求收到空的响应数据')); } } on DioException catch (e) { AppLogger.e('ApiClient', 'postStream 请求失败,路径: $path', e); throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', 'postStream 执行出错,路径: $path', e); throw ApiException(-1, '执行流式请求时发生意外错误: ${e.toString()}'); } } /// 辅助方法:处理字节流,解码,解析 SSE 或单行 JSON 数组,并生成指定类型的流 Stream _processStream({ required Future>> byteStreamFuture, required T Function(Map) fromJson, required String logContext, }) { final controller = StreamController(); int retryCount = 0; const maxRetries = 3; Future processStream() async { try { final byteStream = await byteStreamFuture; final stringStream = utf8.decoder.bind(byteStream); await for (final rawLine in stringStream.transform(const LineSplitter())) { try { final line = rawLine.trim(); if (line.isEmpty) { continue; } if (line.startsWith('data:')) { final eventData = line.substring(5).trim(); if (eventData.isNotEmpty && eventData != '[DONE]') { final json = jsonDecode(eventData); if (json is Map) { final item = fromJson(json); AppLogger.v('ApiClient', '[$logContext] 解析 SSE 数据: ${item.runtimeType}'); if (!controller.isClosed) { controller.add(item); } } else { AppLogger.w('ApiClient', '[$logContext] SSE 数据不是有效的 JSON 对象: $eventData'); } } else if (eventData == '[DONE]') { AppLogger.i('ApiClient', '[$logContext] 收到 SSE 流结束标记 [DONE]'); } } else if (line.startsWith('[') && line.endsWith(']')) { AppLogger.v('ApiClient', '[$logContext] 检测到单行 JSON 数组,尝试解析,长度: ${line.length}'); final decodedList = jsonDecode(line); if (decodedList is List) { int count = 0; for (final itemJson in decodedList) { await Future.delayed(Duration.zero); if (controller.isClosed) break; if (itemJson is Map) { try { final item = fromJson(itemJson); AppLogger.v('ApiClient', '[$logContext] 解析 JSON 数组元素 ${++count}: ${item.runtimeType}'); if (!controller.isClosed) { controller.add(item); } } catch (e, stackTrace) { AppLogger.e( 'ApiClient', '[$logContext] 从 JSON 数组元素转换失败: $itemJson', e, stackTrace); } } else { AppLogger.w('ApiClient', '[$logContext] JSON 数组中的元素不是 Map: $itemJson'); } } AppLogger.i('ApiClient', '[$logContext] 成功处理 $count 个 JSON 数组元素'); } else { AppLogger.w('ApiClient', '[$logContext] 解析为 JSON 但不是列表: "$line"'); } } else { AppLogger.v( 'ApiClient', '[$logContext] 忽略非 SSE 且非 JSON 数组的行: "$line"'); } } catch (e, stackTrace) { AppLogger.e('ApiClient', '[$logContext] 解析流式响应行失败: "$rawLine"', e, stackTrace); } if (controller.isClosed) break; } AppLogger.i('ApiClient', '[$logContext] 流式字符串处理完成'); if (!controller.isClosed) { controller.close(); } } catch (error, stackTrace) { AppLogger.e('ApiClient', '[$logContext] 获取或解码流式字节流失败', error, stackTrace); if (retryCount < maxRetries) { retryCount++; AppLogger.i('ApiClient', '[$logContext] 尝试重试 ($retryCount/$maxRetries)'); await Future.delayed(Duration(seconds: retryCount * 2)); // 指数退避 return processStream(); } if (!controller.isClosed) { final apiError = (error is ApiException) ? error : ApiException( -1, '[$logContext] 启动或解码流式请求失败: ${error.toString()}'); controller.addError(apiError, stackTrace); controller.close(); } } } processStream(); return controller.stream; } /// 基础GET请求方法,返回流 Future>> getStream(String path, {Options? options}) async { try { final response = await _dio.get( path, options: (options ?? Options()).copyWith(responseType: ResponseType.stream), ); if (response.data != null) { return response.data!.stream; } else { AppLogger.w('ApiClient', 'getStream 收到空的响应数据,路径: $path'); return Stream.error(ApiException(-1, '流式请求收到空的响应数据')); } } on DioException catch (e) { AppLogger.e('ApiClient', 'getStream 请求失败,路径: $path', e); throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', 'getStream 执行出错,路径: $path', e); throw ApiException(-1, '执行流式请求时发生意外错误: ${e.toString()}'); } } /// 基础GET请求方法 Future get(String path, {Options? options}) async { try { final response = await _dio.get(path, options: options); return _normalizeResponseData(response.data); } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', 'get 执行出错,路径: $path', e); throw ApiException(-1, '执行 GET 请求时发生意外错误: ${e.toString()}'); } } /// 支持查询参数的GET请求方法 Future getWithParams(String path, {Map? queryParameters, Options? options}) async { try { final response = await _dio.get(path, queryParameters: queryParameters, options: options); return _normalizeResponseData(response.data); } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', 'getWithParams 执行出错,路径: $path', e); throw ApiException(-1, '执行 GET 请求时发生意外错误: ${e.toString()}'); } } /// 基础PUT请求方法 Future put(String path, {dynamic data, Options? options}) async { try { final response = await _dio.put(path, data: data, options: options); return _normalizeResponseData(response.data); } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', 'put 执行出错,路径: $path', e); throw ApiException(-1, '执行 PUT 请求时发生意外错误: ${e.toString()}'); } } /// 基础PATCH请求方法 Future patch(String path, {dynamic data, Options? options}) async { try { final response = await _dio.patch(path, data: data, options: options); return _normalizeResponseData(response.data); } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', 'patch 执行出错,路径: $path', e); throw ApiException(-1, '执行 PATCH 请求时发生意外错误: ${e.toString()}'); } } /// 基础DELETE请求方法 Future delete(String path, {dynamic data, Options? options}) async { try { final response = await _dio.delete(path, data: data, options: options); return _normalizeResponseData(response.data); } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', 'delete 执行出错,路径: $path', e); throw ApiException(-1, '执行 DELETE 请求时发生意外错误: ${e.toString()}'); } } /// 支持查询参数的DELETE请求方法 Future deleteWithParams(String path, {Map? queryParameters, dynamic data, Options? options}) async { try { final response = await _dio.delete(path, queryParameters: queryParameters, data: data, options: options); return _normalizeResponseData(response.data); } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', 'deleteWithParams 执行出错,路径: $path', e); throw ApiException(-1, '执行 DELETE 请求时发生意外错误: ${e.toString()}'); } } //==== 小说相关接口 ====// /// 导入小说文件 Future importNovel(List fileBytes, String fileName) async { try { // 获取当前用户ID final userId = AppConfig.userId; // 创建 MultipartFile final formData = FormData.fromMap({ 'file': MultipartFile.fromBytes( fileBytes, filename: fileName, ), // 添加用户ID字段,虽然后端应该能从token中获取,这里作为备用 if (userId != null) 'userId': userId, }); // 设置接收 JobId 的选项 final options = Options( contentType: 'multipart/form-data', responseType: ResponseType.json, ); // 发送上传请求 final response = await _dio.post( '/novels/import', data: formData, options: options, ); // 响应应该包含一个 jobId if (response.data is Map && response.data.containsKey('jobId')) { return response.data['jobId']; } else { AppLogger.e('ApiClient', '导入小说响应格式不正确: ${response.data}'); throw ApiException(-1, '导入请求响应格式不正确'); } } on DioException catch (e) { AppLogger.e('ApiClient', '导入小说文件失败', e); throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '导入小说文件失败', e); throw ApiException(-1, '导入小说文件失败: ${e.toString()}'); } } /// 取消导入任务 Future cancelImport(String jobId) async { try { AppLogger.i('ApiClient', '发送取消导入任务请求: jobId=$jobId'); // 使用基础POST方法发送取消请求 final response = await post('/novels/import/$jobId/cancel'); if (response is Map && response.containsKey('status')) { final success = response['status'] == 'success'; AppLogger.i('ApiClient', '取消导入任务结果: ${success ? '成功' : '失败'}, jobId=$jobId'); return success; } AppLogger.w('ApiClient', '取消导入任务响应格式不正确: $response'); return false; } catch (e) { AppLogger.e('ApiClient', '取消导入任务失败: jobId=$jobId', e); return false; } } // === 新的三步导入流程API方法 === /// 第一步:上传文件获取预览会话ID Future uploadFileForPreview(List fileBytes, String fileName) async { try { // 获取当前用户ID final userId = AppConfig.userId; // 创建 MultipartFile final formData = FormData.fromMap({ 'file': MultipartFile.fromBytes( fileBytes, filename: fileName, ), // 添加用户ID字段,虽然后端应该能从token中获取,这里作为备用 if (userId != null) 'userId': userId, }); // 设置接收 JSON 的选项 final options = Options( contentType: 'multipart/form-data', responseType: ResponseType.json, ); // 发送上传请求 final response = await _dio.post( '/novels/import/upload-preview', data: formData, options: options, ); // 响应应该包含一个 previewSessionId if (response.data is Map && response.data.containsKey('previewSessionId')) { return response.data['previewSessionId']; } else { AppLogger.e('ApiClient', '上传预览文件响应格式不正确: ${response.data}'); throw ApiException(-1, '上传预览文件响应格式不正确'); } } on DioException catch (e) { AppLogger.e('ApiClient', '上传预览文件失败', e); throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '上传预览文件失败', e); throw ApiException(-1, '上传预览文件失败: ${e.toString()}'); } } /// 第二步:获取导入预览 Future> getImportPreview({ required String fileSessionId, String? customTitle, int? chapterLimit, bool enableSmartContext = true, bool enableAISummary = false, String? aiConfigId, int previewChapterCount = 10, }) async { try { final requestData = { 'fileSessionId': fileSessionId, 'enableSmartContext': enableSmartContext, 'enableAISummary': enableAISummary, 'previewChapterCount': previewChapterCount, if (customTitle != null) 'customTitle': customTitle, if (chapterLimit != null) 'chapterLimit': chapterLimit, if (aiConfigId != null) 'aiConfigId': aiConfigId, }; final response = await _dio.post( '/novels/import/preview', data: requestData, options: Options(responseType: ResponseType.json), ); if (response.data is Map) { return response.data as Map; } else { AppLogger.e('ApiClient', '获取导入预览响应格式不正确: ${response.data}'); throw ApiException(-1, '获取导入预览响应格式不正确'); } } catch (e) { AppLogger.e('ApiClient', '获取导入预览失败', e); if (e is ApiException) { rethrow; } throw ApiException(-1, '获取导入预览失败: ${e.toString()}'); } } /// 第三步:确认并开始导入 Future confirmAndStartImport({ required String previewSessionId, required String finalTitle, List? selectedChapterIndexes, bool enableSmartContext = true, bool enableAISummary = false, String? aiConfigId, }) async { try { final requestData = { 'previewSessionId': previewSessionId, 'finalTitle': finalTitle, 'enableSmartContext': enableSmartContext, 'enableAISummary': enableAISummary, 'acknowledgeRisks': true, if (selectedChapterIndexes != null) 'selectedChapterIndexes': selectedChapterIndexes, if (aiConfigId != null) 'aiConfigId': aiConfigId, if (AppConfig.userId != null) 'userId': AppConfig.userId, }; final response = await _dio.post( '/novels/import/confirm', data: requestData, options: Options(responseType: ResponseType.json), ); if (response.data is Map && (response.data as Map).containsKey('jobId')) { return (response.data as Map)['jobId']; } else { AppLogger.e('ApiClient', '确认导入响应格式不正确: ${response.data}'); throw ApiException(-1, '确认导入响应格式不正确'); } } catch (e) { AppLogger.e('ApiClient', '确认导入失败', e); if (e is ApiException) { rethrow; } throw ApiException(-1, '确认导入失败: ${e.toString()}'); } } /// 清理预览会话 Future cleanupPreviewSession(String previewSessionId) async { try { final requestData = { 'previewSessionId': previewSessionId, }; await _dio.post( '/novels/import/cleanup-preview', data: requestData, options: Options(responseType: ResponseType.json), ); } catch (e) { AppLogger.e('ApiClient', '清理预览会话失败', e); // 清理失败不抛出异常,只记录日志 } } /// 长时间运行的 SSE 连接(适用于小说导入等耗时操作) Stream connectToLongRunningSSE(String jobId) { final controller = StreamController(); final url = '${_dio.options.baseUrl}/novels/import/$jobId/status'; AppLogger.i('ApiClient', '[SSE Connect] 准备连接到: $url'); // 创建一个专用的 Dio 实例 final dioForSSE = Dio(); dioForSSE.options.baseUrl = _dio.options.baseUrl; // 设置认证令牌 final token = AppConfig.authToken; if (token != null) { dioForSSE.options.headers['Authorization'] = 'Bearer $token'; } // 设置 SSE 相关的请求头 dioForSSE.options.headers['Accept'] = 'text/event-stream'; dioForSSE.options.headers['Cache-Control'] = 'no-cache'; dioForSSE.options.headers['Connection'] = 'keep-alive'; // 设置响应类型为流 dioForSSE.options.responseType = ResponseType.stream; // 极大延长超时时间,最多等待3小时 dioForSSE.options.receiveTimeout = const Duration(hours: 3); dioForSSE.options.connectTimeout = const Duration(minutes: 2); // 关闭校验,允许所有状态码 dioForSSE.options.validateStatus = (_) => true; AppLogger.i('ApiClient', '开始连接到长时间运行的 SSE,超时设置为3小时'); // 定义心跳计时器 Timer? heartbeatTimer; DateTime lastEventTime = DateTime.now(); int heartbeatCount = 0; Future connect() async { AppLogger.i('ApiClient', '[SSE Connect] 开始执行 dioForSSE.get(url)...'); try { final responseFuture = dioForSSE.get(url); // Explicitly type ResponseBody AppLogger.i('ApiClient', '[SSE Connect] dioForSSE.get(url) Future 创建成功,等待响应...'); responseFuture.then((response) { AppLogger.i('ApiClient', '[SSE Connect] .then() 回调被执行,状态码: ${response.statusCode}'); if (response.statusCode != 200) { AppLogger.e('ApiClient', '[SSE Error] 连接失败: HTTP ${response.statusCode},响应头: ${response.headers}'); if (!controller.isClosed) { controller.addError(ApiException( response.statusCode ?? -1, '[SSE Error] 连接失败: HTTP ${response.statusCode}')); controller.close(); } return; } AppLogger.i('ApiClient', '[SSE Connect] 连接成功,开始接收事件,响应头: ${response.headers}'); final responseBody = response.data; if (responseBody == null) { AppLogger.e('ApiClient', '[SSE Error] 响应体或流为空'); if (!controller.isClosed) { controller.addError(ApiException(-1, '[SSE Error] 响应体或流为空')); controller.close(); } return; } final stream = responseBody.stream; AppLogger.i('ApiClient', '[SSE Connect] 数据流已获取,设置心跳和监听器...'); // 心跳检测逻辑 (保持不变) lastEventTime = DateTime.now(); // Reset last event time on successful connect heartbeatTimer?.cancel(); // Cancel previous timer if any heartbeatTimer = Timer.periodic(const Duration(seconds: 15), (timer) { // ... (heartbeat logic as before) ... final now = DateTime.now(); final difference = now.difference(lastEventTime); heartbeatCount++; AppLogger.i('ApiClient', '[SSE Heartbeat] #$heartbeatCount: 距上次事件 ${difference.inSeconds} 秒'); if (difference.inMinutes >= 2 && !controller.isClosed) { AppLogger.w('ApiClient', '[SSE Heartbeat] 已 ${difference.inMinutes} 分钟未收到事件,发送本地进度更新'); controller.add(ImportStatus( status: 'PROCESSING', message: '导入处理中,已等待 ${difference.inMinutes} 分钟...' )); if (difference.inMinutes >= 5) { AppLogger.e('ApiClient', '[SSE Heartbeat] 已 ${difference.inMinutes} 分钟未收到事件,关闭连接'); if (!controller.isClosed) { controller.addError(ApiException(-1, '[SSE Error] 连接超时')); controller.close(); // Closing the controller will trigger onDone/onError } timer.cancel(); // Stop this timer } } }); // Stream 监听逻辑 (基本保持不变, 增加日志) String buffer = ''; stream.listen( (data) { lastEventTime = DateTime.now(); // Update time on receiving data AppLogger.v('ApiClient', '[SSE Data] 收到原始数据块 (长度: ${data.length})'); try { String chunk = utf8.decode(data); AppLogger.i('ApiClient', '[SSE Data] 解码后数据块: $chunk'); buffer += chunk; while (buffer.contains('\n\n')) { int endIndex = buffer.indexOf('\n\n'); String message = buffer.substring(0, endIndex).trim(); buffer = buffer.substring(endIndex + 2); AppLogger.i('ApiClient', '[SSE Parse] 解析出完整消息: $message'); // ... (message parsing logic as before) ... List lines = message.split('\n'); Map eventData = {}; for (String line in lines) { if (line.startsWith('id:')) { eventData['id'] = line.substring(3).trim(); } else if (line.startsWith('event:')) { eventData['event'] = line.substring(6).trim(); } else if (line.startsWith('data:')) { eventData['data'] = line.substring(5).trim(); } else if (line.startsWith(':')) { AppLogger.i('ApiClient', '[SSE Comment] 收到服务器心跳注释: ${line.substring(1).trim()}'); } } if (eventData.containsKey('data')) { try { final json = jsonDecode(eventData['data']!); if (json is Map) { final status = ImportStatus.fromJson(json); AppLogger.i('ApiClient', '[SSE Status] 收到状态: ${status.status} - ${status.message}'); if (!controller.isClosed) controller.add(status); if (status.status == 'COMPLETED' || status.status == 'FAILED') { AppLogger.i('ApiClient', '[SSE Status] 收到最终状态,关闭连接'); heartbeatTimer?.cancel(); if (!controller.isClosed) controller.close(); } } } catch (e, stack) { AppLogger.e('ApiClient', '[SSE Parse] 解析 SSE data 失败: ${eventData['data']}', e, stack); } } else { // ... (direct message parsing logic as before) ... if (message.isNotEmpty && message != '[DONE]') { try { Map? json; if (message.startsWith('{') && message.endsWith('}')) { json = jsonDecode(message) as Map?; } if (json != null && json.containsKey('status')) { final status = ImportStatus.fromJson(json); AppLogger.i('ApiClient', '[SSE Parse] 直接解析消息为状态: ${status.status}'); if (!controller.isClosed) controller.add(status); if (status.status == 'COMPLETED' || status.status == 'FAILED') { AppLogger.i('ApiClient', '[SSE Status] 收到最终状态,关闭连接'); heartbeatTimer?.cancel(); if (!controller.isClosed) controller.close(); } } } catch (e) { // Ignore non-JSON messages AppLogger.v('ApiClient', '[SSE Parse] 消息不是有效JSON,忽略: $message'); } } } } } catch (e, stack) { AppLogger.e('ApiClient', '[SSE Error] 处理数据块失败', e, stack); } }, onError: (e, stack) { AppLogger.e('ApiClient', '[SSE Error] 流错误', e, stack); heartbeatTimer?.cancel(); if (!controller.isClosed) { controller.addError( e is ApiException ? e : ApiException(-1, '[SSE Error] 读取流错误: $e'), stack); controller.close(); } }, onDone: () { AppLogger.i('ApiClient', '[SSE Connect] 流已关闭 (onDone)'); heartbeatTimer?.cancel(); if (!controller.isClosed) { controller.close(); } }, ); }).catchError((e, stack) { // 这个 catchError 主要捕获 Future 本身的错误,比如 dio().get() 失败 AppLogger.e('ApiClient', '[SSE Error] dioForSSE.get(url) Future 失败', e, stack); heartbeatTimer?.cancel(); if (!controller.isClosed) { controller.addError( e is ApiException ? e : ApiException(-1, '[SSE Error] 连接或读取流失败: $e'), stack); controller.close(); } }); } catch (e, stack) { // 这个 catch 主要捕获调用 dioForSSE.get(url) 时的同步错误 AppLogger.e('ApiClient', '[SSE Error] 调用 dioForSSE.get(url) 时发生同步错误', e, stack); heartbeatTimer?.cancel(); // Ensure timer is cancelled if (!controller.isClosed) { controller.addError(ApiException(-1, '[SSE Error] 启动连接时出错: $e'), stack); controller.close(); } } } // Start the connection process connect(); // 当流被取消时,确保清理资源 (保持不变) controller.onCancel = () { heartbeatTimer?.cancel(); AppLogger.i('ApiClient', '[SSE Connect] 流已被外部取消 (onCancel)'); // Dio 会自动取消请求,但我们确保计时器停止 }; return controller.stream; } /// 获取小说导入状态 SSE 流(长时间运行版本) Stream getImportStatusStream(String jobId) { AppLogger.i('ApiClient', '获取导入状态流,使用长时间运行的 SSE 连接'); // 创建一个StreamController,用于处理自动重试逻辑 final controller = StreamController(); int retryCount = 0; const maxRetries = 3; StreamSubscription? subscription; // 定义连接函数 void connect() { AppLogger.i('ApiClient', '连接到导入状态流,尝试 #${retryCount + 1}'); subscription = connectToLongRunningSSE(jobId).listen( (status) { // 正常转发状态更新 controller.add(status); // 如果是完成或失败状态,关闭控制器 if (status.status == 'COMPLETED' || status.status == 'FAILED') { AppLogger.i('ApiClient', '收到最终状态:${status.status},关闭状态流'); if (!controller.isClosed) { controller.close(); } } }, onError: (error, stack) { AppLogger.e('ApiClient', '导入状态流出错', error, stack); // 如果还可以重试,则重试 if (retryCount < maxRetries) { retryCount++; // 指数退避策略 final delay = Duration(seconds: retryCount * 3); AppLogger.i('ApiClient', '将在 ${delay.inSeconds} 秒后重试连接 ($retryCount/$maxRetries)'); // 延迟后重试 Future.delayed(delay, () { if (!controller.isClosed) { connect(); } }); } else { // 超过重试次数,将错误转发给上层 AppLogger.e('ApiClient', '导入状态流重试耗尽,传递错误'); if (!controller.isClosed) { controller.addError(error, stack); controller.close(); } } }, onDone: () { AppLogger.i('ApiClient', '导入状态流已完成'); if (!controller.isClosed) { controller.close(); } }, ); } // 启动连接 connect(); // 当流被取消时清理资源 controller.onCancel = () { subscription?.cancel(); AppLogger.i('ApiClient', '导入状态流已被取消'); }; return controller.stream; } /// 根据作者ID获取小说列表 Future getNovelsByAuthor(String authorId) async { return post('/novels/get-by-author', data: {'authorId': authorId}); } /// 根据ID获取小说详情 Future getNovelDetailById(String id) async { return post('/novels/get-with-scenes', data: {'id': id}); } /// 分页加载小说详情和场景内容 /// 基于上次编辑章节为中心,获取前后指定数量的章节及其场景内容 Future getNovelWithPaginatedScenes(String novelId, String lastEditedChapterId, {int chaptersLimit = 5}) async { try { AppLogger.i('ApiClient', '分页加载小说详情: $novelId, 中心章节: $lastEditedChapterId, 限制: $chaptersLimit'); final response = await post('/novels/get-with-paginated-scenes', data: { 'novelId': novelId, 'lastEditedChapterId': lastEditedChapterId, 'chaptersLimit': chaptersLimit }); return response; } catch (e) { AppLogger.e('ApiClient', '分页加载小说详情失败', e); rethrow; } } /// 加载更多场景内容 /// 根据方向(向上或向下或中心)加载更多章节的场景内容 /// direction可以是:up、down或center /// - up: 加载fromChapterId之前的章节 /// - down: 加载fromChapterId之后的章节 /// - center: 只加载fromChapterId章节或前后各加载几章 Future loadMoreScenes(String novelId, String actId, String fromChapterId, String direction, {int chaptersLimit = 3}) async { try { AppLogger.i('ApiClient', '加载更多场景: $novelId, 卷: $actId, 从章节: $fromChapterId, 方向: $direction, 限制: $chaptersLimit'); final response = await post('/novels/load-more-scenes', data: { 'novelId': novelId, 'actId': actId, 'fromChapterId': fromChapterId, 'direction': direction, 'chaptersLimit': chaptersLimit }); return response; } catch (e) { AppLogger.e('ApiClient', '加载更多场景失败', e); rethrow; } } /// 获取当前章节后面指定数量的章节和场景内容 /// 允许跨卷加载,专门用于阅读器的分批加载 Future getChaptersAfter(String novelId, String currentChapterId, {int chaptersLimit = 3, required bool includeCurrentChapter}) async { try { AppLogger.i('ApiClient', '获取后续章节: $novelId, 当前章节: $currentChapterId, 限制: $chaptersLimit, includeCurrentChapter: $includeCurrentChapter'); final response = await post('/novels/get-chapters-after', data: { 'novelId': novelId, 'currentChapterId': currentChapterId, 'chaptersLimit': chaptersLimit, 'includeCurrentChapter': includeCurrentChapter }); return response; } catch (e) { AppLogger.e('ApiClient', '获取后续章节失败', e); rethrow; } } /// 创建小说 Future createNovel(Map novelData) async { return post('/novels/create', data: novelData); } /// 更新小说 Future updateNovel(Map novelData) async { try { final response = await post('/novels/update', data: novelData); return response; } catch (e) { AppLogger.e('Services/api_service/base/api_client', '更新小说数据失败', e); rethrow; } } /// 更新小说及其场景内容 Future updateNovelWithScenes( Map novelWithScenesData) async { AppLogger.i('/novels/update-with-scenes', '开始更新小说及场景数据'); AppLogger.d('/novels/update-with-scenes', '发送的数据: $novelWithScenesData'); try { final response = await post('/novels/update-with-scenes', data: novelWithScenesData); AppLogger.i('/novels/update-with-scenes', '更新成功'); return response; } catch (e) { AppLogger.e('/novels/update-with-scenes', '更新小说及场景数据失败,发送的数据: $novelWithScenesData', e); rethrow; } } /// 删除小说 Future deleteNovel(String id) async { return post('/novels/delete', data: {'id': id}); } /// 根据标题搜索小说 Future searchNovelsByTitle(String title) async { return post('/novels/search-by-title', data: {'title': title}); } //==== 场景相关接口 ====// /// 根据ID获取场景内容 Future getSceneById( String novelId, String chapterId, String sceneId) async { try { final response = await post('/scenes/get', data: { 'id': sceneId, }); return response; } catch (e) { AppLogger.e('Services/api_service/base/api_client', '获取场景数据失败', e); rethrow; } } /// 根据章节ID获取所有场景 Future getScenesByChapter(String novelId, String chapterId) async { return post('/scenes/get-by-chapter', data: {'novelId': novelId, 'chapterId': chapterId}); } /// 创建场景,未使用 Future createScene(Map sceneData) async { return post('/scenes/create', data: sceneData); } /// 更新场景 (调用后端的 upsert 接口) Future updateScene(Map sceneData) async { try { final response = await post('/scenes/upsert', data: sceneData); return response; } catch (e) { AppLogger.e( 'Services/api_service/base/api_client', '更新/创建场景数据失败', e); // 更新日志消息 rethrow; } } /// 更新场景并保存历史版本 Future updateSceneWithHistory(String novelId, String chapterId, String sceneId, String content, String userId, String reason) async { return post('/scenes/update-with-history', data: { 'novelId': novelId, 'chapterId': chapterId, 'sceneId': sceneId, 'content': content, 'userId': userId, 'reason': reason }); } /// 获取场景历史版本 Future getSceneHistory( String novelId, String chapterId, String sceneId) async { return post('/scenes/history', data: {'novelId': novelId, 'chapterId': chapterId, 'sceneId': sceneId}); } /// 恢复场景历史版本 Future restoreSceneVersion(String novelId, String chapterId, String sceneId, int historyIndex, String userId, String reason) async { return post('/scenes/restore', data: { 'novelId': novelId, 'chapterId': chapterId, 'sceneId': sceneId, 'historyIndex': historyIndex, 'userId': userId, 'reason': reason }); } /// 比较场景版本 Future compareSceneVersions(String novelId, String chapterId, String sceneId, int versionIndex1, int versionIndex2) async { return post('/scenes/compare', data: { 'novelId': novelId, 'chapterId': chapterId, 'sceneId': sceneId, 'versionIndex1': versionIndex1, 'versionIndex2': versionIndex2 }); } //==== 编辑器相关接口 ====// /// 获取编辑器内容 Future getEditorContent( String novelId, String chapterId, String sceneId) async { return post('/editor/get-content', data: {'novelId': novelId, 'chapterId': chapterId, 'sceneId': sceneId}); } /// 保存编辑器内容 Future saveEditorContent( String novelId, String chapterId, Map content) async { return post('/editor/save-content', data: {'novelId': novelId, 'chapterId': chapterId, 'content': content}); } /// 获取修订历史 Future getRevisionHistory(String novelId, String chapterId) async { return post('/editor/get-revisions', data: {'novelId': novelId, 'chapterId': chapterId}); } /// 创建修订版本 Future createRevision( String novelId, String chapterId, Map revision) async { return post('/editor/create-revision', data: { 'novelId': novelId, 'chapterId': chapterId, 'revision': revision }); } /// 应用修订版本 Future applyRevision( String novelId, String chapterId, String revisionId) async { return post('/editor/apply-revision', data: { 'novelId': novelId, 'chapterId': chapterId, 'revisionId': revisionId }); } //==== 用户编辑器设置相关接口 ====// /// 获取用户编辑器设置 Future getUserEditorSettings(String userId) async { return get('/api/user-editor-settings/$userId'); } /// 保存用户编辑器设置 Future saveUserEditorSettings(String userId, Map settings) async { return post('/api/user-editor-settings/$userId', data: settings); } /// 更新用户编辑器设置 Future updateUserEditorSettings(String userId, Map settings) async { return patch('/api/user-editor-settings/$userId', data: settings); } /// 重置用户编辑器设置为默认值 Future resetUserEditorSettings(String userId) async { return post('/api/user-editor-settings/$userId/reset'); } /// 删除用户编辑器设置 Future deleteUserEditorSettings(String userId) async { return delete('/api/user-editor-settings/$userId'); } //==== AI 聊天相关接口 (新) ====// /// 创建 AI 聊天会话 (非流式) Future createAiChatSession({ required String userId, required String novelId, String? modelName, Map? metadata, }) async { try { final response = await post('/ai-chat/sessions/create', data: { 'userId': userId, 'novelId': novelId, 'modelName': modelName, 'metadata': metadata, }); return ChatSession.fromJson(response); } catch (e) { AppLogger.e('ApiClient', '创建 AI 会话失败', e); rethrow; } } /// 获取特定 AI 会话 (非流式) - 现在返回包含AI配置的响应 Future> getAiChatSessionWithConfig(String userId, String sessionId, {String? novelId}) async { try { AppLogger.d('ApiClient', '获取AI会话(含配置): userId=$userId, sessionId=$sessionId, novelId=$novelId'); final requestData = { 'userId': userId, 'sessionId': sessionId, }; // 🚀 添加novelId支持 if (novelId != null) { requestData['novelId'] = novelId; } final response = await post('/ai-chat/sessions/get', data: requestData); if (response is Map) { // 解析会话信息 final sessionData = response['session']; if (sessionData != null) { final session = ChatSession.fromJson(sessionData); AppLogger.d('ApiClient', '解析会话成功: ${session.title}, hasAIConfig=${response["aiConfig"] != null}'); return { 'session': session, 'aiConfig': response['aiConfig'], 'presetId': response['presetId'], }; } else { throw ApiException(-1, '响应中没有找到会话数据'); } } else { throw ApiException(-1, '响应格式不正确: $response'); } } catch (e) { AppLogger.e('ApiClient', '获取 AI 会话(含配置)失败 (ID: $sessionId)', e); rethrow; } } /// 获取特定 AI 会话 (非流式) - 兼容旧版本 Future getAiChatSession(String userId, String sessionId, {String? novelId}) async { final response = await getAiChatSessionWithConfig(userId, sessionId, novelId: novelId); return response['session'] as ChatSession; } /// 获取用户的所有 AI 会话 (流式) /// /// 返回 ChatSession 流 Stream listAiChatUserSessionsStream(String userId, {int page = 0, int size = 100, String? novelId}) { final requestData = {'userId': userId}; // 🚀 添加novelId支持 if (novelId != null) { requestData['novelId'] = novelId; } final byteStreamFuture = postStream('/ai-chat/sessions/list', data: requestData); return _processStream( byteStreamFuture: byteStreamFuture, fromJson: ChatSession.fromJson, logContext: 'listAiChatUserSessionsStream', ); } /// 更新 AI 会话 (非流式) Future updateAiChatSession({ required String userId, required String sessionId, required Map updates, String? novelId, }) async { try { final requestData = { 'userId': userId, 'sessionId': sessionId, 'updates': updates, }; // 🚀 添加novelId支持 if (novelId != null) { requestData['novelId'] = novelId; } final response = await post('/ai-chat/sessions/update', data: requestData); return ChatSession.fromJson(response); } catch (e) { AppLogger.e('ApiClient', '更新 AI 会话失败 (ID: $sessionId)', e); rethrow; } } /// 删除 AI 会话 (非流式) Future deleteAiChatSession(String userId, String sessionId, {String? novelId}) async { try { final requestData = { 'userId': userId, 'sessionId': sessionId, }; // 🚀 添加novelId支持 if (novelId != null) { requestData['novelId'] = novelId; } await post('/ai-chat/sessions/delete', data: requestData); } catch (e) { AppLogger.e('ApiClient', '删除 AI 会话失败 (ID: $sessionId)', e); rethrow; } } /// 发送 AI 消息 (非流式) Future sendAiChatMessage({ required String userId, required String sessionId, required String content, Map? metadata, String? novelId, // 🚀 添加novelId支持 }) async { try { final requestData = { 'userId': userId, 'sessionId': sessionId, 'content': content, 'metadata': metadata, }; // 🚀 添加novelId支持 if (novelId != null) { requestData['novelId'] = novelId; } final response = await post('/ai-chat/messages/send', data: requestData); return ChatMessage.fromJson(response); } catch (e) { AppLogger.e('ApiClient', '发送 AI 消息失败 (SessionID: $sessionId)', e); rethrow; } } /// 流式发送 AI 消息 /// /// 返回解析后的 ChatMessage 流 /// 如果提供了config,会在发送消息的同时保存配置 Stream streamAiChatMessage({ required String userId, required String sessionId, required String content, Map? metadata, Map? config, // 🚀 新增:AI配置参数 String? novelId, // 🚀 添加novelId支持 }) { // 🚀 构建请求数据,包含配置信息 final requestData = { 'userId': userId, 'sessionId': sessionId, 'content': content, 'metadata': metadata, }; // 🚀 如果有配置,添加到请求中 if (config != null) { requestData['config'] = config; } // 🚀 添加novelId支持 if (novelId != null) { requestData['novelId'] = novelId; } final byteStreamFuture = postStream('/ai-chat/messages/stream', data: requestData); return _processStream( byteStreamFuture: byteStreamFuture, fromJson: ChatMessage.fromJson, logContext: 'streamAiChatMessage', ); } /// 获取 AI 会话消息历史 (流式) /// /// 返回 ChatMessage 流 Stream getAiChatMessageHistoryStream( String userId, String sessionId, {int limit = 100, String? novelId}) { final requestData = { 'userId': userId, 'sessionId': sessionId, 'limit': limit, }; // 🚀 添加novelId支持 if (novelId != null) { requestData['novelId'] = novelId; } final byteStreamFuture = postStream('/ai-chat/messages/history', data: requestData); return _processStream( byteStreamFuture: byteStreamFuture, fromJson: ChatMessage.fromJson, logContext: 'getAiChatMessageHistoryStream', ); } /// 获取特定 AI 消息 (非流式) Future getAiChatMessage(String userId, String messageId) async { try { final response = await post('/ai-chat/messages/get', data: { 'userId': userId, 'messageId': messageId, }); return ChatMessage.fromJson(response); } catch (e) { AppLogger.e('ApiClient', '获取 AI 消息失败 (ID: $messageId)', e); rethrow; } } /// 删除 AI 消息 (非流式) Future deleteAiChatMessage(String userId, String messageId) async { try { await post('/ai-chat/messages/delete', data: { 'userId': userId, 'messageId': messageId, }); } catch (e) { AppLogger.e('ApiClient', '删除 AI 消息失败 (ID: $messageId)', e); rethrow; } } /// 获取 AI 会话消息数量 (非流式) Future countAiChatSessionMessages(String sessionId) async { try { final response = await post('/ai-chat/messages/count', data: {'id': sessionId}); if (response is int) { return response; } else if (response is String) { return int.tryParse(response) ?? (throw ApiException(-1, '无法解析消息数量响应: $response')); } else if (response is Map && response.containsKey('count')) { final count = response['count']; if (count is int) return count; } throw ApiException(-1, '无法解析消息数量响应: $response'); } catch (e) { AppLogger.e('ApiClient', '获取消息数量失败 (SessionID: $sessionId)', e); rethrow; } } /// 获取用户 AI 会话数量 (非流式) Future countAiChatUserSessions(String userId) async { try { final response = await post('/ai-chat/sessions/count', data: {'id': userId}); if (response is int) { return response; } else if (response is String) { return int.tryParse(response) ?? (throw ApiException(-1, '无法解析会话数量响应: $response')); } else if (response is Map && response.containsKey('count')) { final count = response['count']; if (count is int) return count; } throw ApiException(-1, '无法解析会话数量响应: $response'); } catch (e) { AppLogger.e('ApiClient', '获取用户会话数量失败 (UserID: $userId)', e); rethrow; } } /// 获取会话的AI配置 (非流式) Future?> getAiChatSessionConfig(String userId, String sessionId) async { try { AppLogger.d('ApiClient', '获取会话AI配置: userId=$userId, sessionId=$sessionId'); final response = await post('/ai-chat/sessions/config/get', data: { 'userId': userId, 'sessionId': sessionId, }); if (response is Map) { AppLogger.d('ApiClient', '获取会话AI配置响应: hasConfig=${response['config'] != null}, presetId=${response['presetId']}'); return response; // 返回完整响应,包含config、sessionId、presetId等字段 } return null; } catch (e) { AppLogger.e('ApiClient', '获取会话AI配置失败 (SessionID: $sessionId)', e); return null; // 配置获取失败不应该阻止会话加载 } } /// 保存会话的AI配置 (非流式) Future saveAiChatSessionConfig(String userId, String sessionId, Map config) async { try { final response = await post('/ai-chat/sessions/config/save', data: { 'userId': userId, 'sessionId': sessionId, 'config': config, }); if (response is Map) { return response['success'] == true; } return false; } catch (e) { AppLogger.e('ApiClient', '保存会话AI配置失败 (SessionID: $sessionId)', e); return false; } } //==== 用户 AI 模型配置相关接口 (新) ====// final String _userAIConfigBasePath = '/user-ai-configs'; /// 获取系统支持的 AI 提供商列表 Future> listAIProviders() async { final path = '$_userAIConfigBasePath/providers/list'; try { // 后端返回 Flux,在 Dio 拦截器/转换器中转为 List final responseData = await post(path); if (responseData is List) { // 确保列表中的每个元素都转换为 String final providers = responseData.map((item) => item.toString()).toList(); return providers; } else { AppLogger.e('ApiClient', 'listAIProviders 响应格式错误: $responseData'); throw ApiException(-1, '获取可用提供商列表响应格式错误'); } } catch (e) { AppLogger.e('ApiClient', '获取可用 AI 提供商列表失败', e); rethrow; // post 方法已经处理了 DioException } } /// 获取指定 AI 提供商支持的模型列表 Future> listAIModelsForProvider( {required String provider}) async { final path = '$_userAIConfigBasePath/providers/models/list'; final body = {'provider': provider}; try { // Backend returns Flux, Dio post likely collects it into List final responseData = await post(path, data: body); if (responseData is List) { // Parse the list of JSON maps into a list of ModelInfo objects final models = responseData .map((json) => ModelInfo.fromJson(json as Map)) .toList(); return models; } else { AppLogger.e( 'ApiClient', 'listAIModelsForProvider 响应格式错误: $responseData'); throw ApiException(-1, '获取模型列表响应格式错误'); } } catch (e) { AppLogger.e('ApiClient', '获取提供商 $provider 的模型列表失败', e); rethrow; } } /// 添加新的用户 AI 模型配置 Future addAIConfiguration({ required String userId, required String provider, required String modelName, String? alias, required String apiKey, String? apiEndpoint, }) async { final path = '$_userAIConfigBasePath/users/$userId/create'; final body = { 'provider': provider, 'modelName': modelName, 'apiKey': apiKey, // API Key 由后端处理加密 if (alias != null) 'alias': alias, if (apiEndpoint != null) 'apiEndpoint': apiEndpoint, }; try { final responseData = await post(path, data: body); AppLogger.i('ApiClient', '添加配置成功,响应数据: $responseData'); if (responseData is Map) { // 添加字段检查日志 AppLogger.d('ApiClient', '响应字段检查:'); AppLogger.d('ApiClient', ' id: ${responseData['id']} (${responseData['id'].runtimeType})'); AppLogger.d('ApiClient', ' userId: ${responseData['userId']} (${responseData['userId'].runtimeType})'); AppLogger.d('ApiClient', ' provider: ${responseData['provider']} (${responseData['provider'].runtimeType})'); AppLogger.d('ApiClient', ' modelName: ${responseData['modelName']} (${responseData['modelName'].runtimeType})'); AppLogger.d('ApiClient', ' alias: ${responseData['alias']} (${responseData['alias'].runtimeType})'); AppLogger.d('ApiClient', ' apiEndpoint: ${responseData['apiEndpoint']} (${responseData['apiEndpoint'].runtimeType})'); AppLogger.d('ApiClient', ' isValidated: ${responseData['isValidated']} (${responseData['isValidated'].runtimeType})'); AppLogger.d('ApiClient', ' isDefault: ${responseData['isDefault']} (${responseData['isDefault'].runtimeType})'); AppLogger.d('ApiClient', ' createdAt: ${responseData['createdAt']} (${responseData['createdAt'].runtimeType})'); AppLogger.d('ApiClient', ' updatedAt: ${responseData['updatedAt']} (${responseData['updatedAt'].runtimeType})'); AppLogger.d('ApiClient', ' apiKey: ${responseData['apiKey']} (${responseData['apiKey'].runtimeType})'); return UserAIModelConfigModel.fromJson(responseData); } else { AppLogger.e('ApiClient', 'addAIConfiguration 响应格式错误: $responseData'); throw ApiException(-1, '添加配置响应格式错误'); } } catch (e) { AppLogger.e('ApiClient', '添加 AI 配置失败 for user $userId', e); rethrow; } } /// 获取用户所有AI配置(普通接口,不含解密的API密钥) Future> listAIConfigurations({ required String userId, bool? validatedOnly, }) async { final path = '$_userAIConfigBasePath/users/$userId/list'; final body = {}; if (validatedOnly != null) { body['validatedOnly'] = validatedOnly; } try { // 如果 body 为空,data 应该传 null final responseData = await post(path, data: body.isEmpty ? null : body); if (responseData is List) { final configs = responseData .map((json) => UserAIModelConfigModel.fromJson(json as Map)) .toList(); return configs; } else { AppLogger.e('ApiClient', 'listAIConfigurations 响应格式错误: $responseData'); throw ApiException(-1, '列出配置响应格式错误'); } } catch (e) { AppLogger.e('ApiClient', '列出 AI 配置失败 for user $userId', e); rethrow; } } /// 获取用户所有AI配置,包含解密后的API密钥 Future> listAIConfigurationsWithDecryptedKeys({ required String userId, bool? validatedOnly, }) async { final path = '$_userAIConfigBasePath/users/$userId/list-with-api-keys'; final body = {}; if (validatedOnly != null) { body['validatedOnly'] = validatedOnly; } try { // 如果 body 为空,data 应该传 null final responseData = await post(path, data: body.isEmpty ? null : body); if (responseData is List) { final configs = responseData .map((json) => UserAIModelConfigModel.fromJson(json as Map)) .toList(); return configs; } else { AppLogger.e('ApiClient', 'listAIConfigurationsWithDecryptedKeys 响应格式错误: $responseData'); throw ApiException(-1, '获取带解密API密钥的配置列表响应格式错误'); } } catch (e) { AppLogger.e('ApiClient', '获取带解密API密钥的AI配置列表失败 for user $userId', e); rethrow; } } /// 获取指定 ID 的用户 AI 模型配置 Future getAIConfigurationById({ required String userId, required String configId, }) async { final path = '$_userAIConfigBasePath/users/$userId/get/$configId'; try { // POST with no body final responseData = await post(path); if (responseData is Map) { return UserAIModelConfigModel.fromJson(responseData); } else { AppLogger.e('ApiClient', 'getAIConfigurationById 响应格式错误 ($userId/$configId): $responseData'); throw ApiException(-1, '获取配置详情响应格式错误'); } } catch (e) { AppLogger.e('ApiClient', '获取 AI 配置失败 ($userId / $configId)', e); rethrow; } } /// 更新指定 ID 的用户 AI 模型配置 Future updateAIConfiguration({ required String userId, required String configId, String? alias, String? apiKey, String? apiEndpoint, }) async { final path = '$_userAIConfigBasePath/users/$userId/update/$configId'; final body = {}; if (alias != null) body['alias'] = alias; if (apiKey != null) body['apiKey'] = apiKey; // 明文发送 if (apiEndpoint != null) body['apiEndpoint'] = apiEndpoint; // 前端仓库层应该已经做了空检查,但以防万一 if (body.isEmpty) { AppLogger.w('ApiClient', '尝试更新配置但没有提供字段 ($userId/$configId)'); // 可以选择抛出错误或返回当前配置(需要额外调用 get) // 这里选择继续发送请求,让后端处理或返回错误 // throw ApiException(-1, 'Update called with no fields to update'); } try { final responseData = await post(path, data: body); if (responseData is Map) { return UserAIModelConfigModel.fromJson(responseData); } else { AppLogger.e('ApiClient', 'updateAIConfiguration 响应格式错误 ($userId/$configId): $responseData'); throw ApiException(-1, '更新配置响应格式错误'); } } catch (e) { AppLogger.e('ApiClient', '更新 AI 配置失败 ($userId / $configId)', e); rethrow; } } /// 删除指定 ID 的用户 AI 模型配置 Future deleteAIConfiguration({ required String userId, required String configId, }) async { final path = '$_userAIConfigBasePath/users/$userId/delete/$configId'; try { // POST with no body. Expect 204 No Content for success. // Dio's post method should handle 204 correctly (doesn't throw by default). // The response.data might be null or empty string for 204. await post(path); // 不需要检查返回值,如果 post 没抛异常就认为成功 } catch (e) { AppLogger.e('ApiClient', '删除 AI 配置失败 ($userId / $configId)', e); // 如果是 404 Not Found 等,post 会抛出 ApiException rethrow; } } /// 手动触发指定配置的 API Key 验证 Future validateAIConfiguration({ required String userId, required String configId, }) async { final path = '$_userAIConfigBasePath/users/$userId/validate/$configId'; try { // POST with no body final responseData = await post(path); if (responseData is Map) { return UserAIModelConfigModel.fromJson(responseData); } else { AppLogger.e('ApiClient', 'validateAIConfiguration 响应格式错误 ($userId/$configId): $responseData'); throw ApiException(-1, '验证配置响应格式错误'); } } catch (e) { AppLogger.e('ApiClient', '验证 AI 配置失败 ($userId / $configId)', e); rethrow; } } /// 设置指定配置为用户的默认模型 Future setDefaultAIConfiguration({ required String userId, required String configId, }) async { final path = '$_userAIConfigBasePath/users/$userId/set-default/$configId'; try { // POST with no body final responseData = await post(path); if (responseData is Map) { return UserAIModelConfigModel.fromJson(responseData); } else { AppLogger.e('ApiClient', 'setDefaultAIConfiguration 响应格式错误 ($userId/$configId): $responseData'); throw ApiException(-1, '设置默认配置响应格式错误'); } } catch (e) { AppLogger.e('ApiClient', '设置默认 AI 配置失败 ($userId / $configId)', e); rethrow; } } /// 获取提供商的模型列表能力 Future getProviderCapability(String providerName) async { try { final response = await _dio.get( '/api/models/providers/$providerName/capability', ); return response.data ?? 'NO_LISTING'; } on DioException catch (e) { AppLogger.e('ApiClient', '获取提供商能力失败,provider: $providerName', e); throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '获取提供商能力时发生意外错误,provider: $providerName', e); throw ApiException(-1, '获取提供商能力失败: ${e.toString()}'); } } /// 使用API密钥获取指定提供商的模型列表 Future> listAIModelsWithApiKey({ required String provider, required String apiKey, String? apiEndpoint, }) async { final path = '/api/models/providers/$provider/info/auth'; // Correct endpoint for auth models try { Map queryParams = { 'apiKey': apiKey, }; if (apiEndpoint != null && apiEndpoint.isNotEmpty) { queryParams['apiEndpoint'] = apiEndpoint; } // Use _dio.get directly to pass queryParameters final response = await _dio.get(path, queryParameters: queryParams); final responseData = response.data; if (responseData is List) { // Parse the list of JSON maps into a list of ModelInfo objects final models = responseData .map((json) => ModelInfo.fromJson(json as Map)) .toList(); return models; } else { AppLogger.w('ApiClient', '使用API密钥获取模型列表返回格式不正确: $responseData'); return []; // Return empty list on format error } } on DioException catch (e) { AppLogger.e('ApiClient', '使用API密钥获取模型列表失败,provider: $provider', e); throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '使用API密钥获取模型列表时发生意外错误,provider: $provider', e); throw ApiException(-1, '使用API密钥获取模型列表失败: ${e.toString()}'); } } //==== 小说片段相关接口 ====// /// 创建片段 Future createSnippet(Map snippetData) async { try { AppLogger.d('ApiClient', '创建片段: $snippetData'); final response = await post('/novel-snippets/create', data: snippetData); return response; } catch (e) { AppLogger.e('ApiClient', '创建片段失败', e); rethrow; } } /// 获取小说的所有片段(分页) Future getSnippetsByNovelId(String novelId, {int page = 0, int size = 20}) async { try { final response = await post('/novel-snippets/get-by-novel', data: { 'novelId': novelId, 'page': page, 'size': size, }); return response; } catch (e) { AppLogger.e('ApiClient', '获取小说片段列表失败: novelId=$novelId', e); rethrow; } } /// 获取片段详情 Future getSnippetDetail(String snippetId) async { try { final response = await post('/novel-snippets/get-detail', data: { 'snippetId': snippetId, }); return response; } catch (e) { AppLogger.e('ApiClient', '获取片段详情失败: snippetId=$snippetId', e); rethrow; } } /// 更新片段内容 Future updateSnippetContent(Map contentData) async { try { AppLogger.d('ApiClient', '更新片段内容: $contentData'); final response = await post('/novel-snippets/update-content', data: contentData); return response; } catch (e) { AppLogger.e('ApiClient', '更新片段内容失败', e); rethrow; } } /// 更新片段标题 Future updateSnippetTitle(Map titleData) async { try { AppLogger.d('ApiClient', '更新片段标题: $titleData'); final response = await post('/novel-snippets/update-title', data: titleData); return response; } catch (e) { AppLogger.e('ApiClient', '更新片段标题失败', e); rethrow; } } /// 收藏/取消收藏片段 Future updateSnippetFavorite(Map favoriteData) async { try { AppLogger.d('ApiClient', '更新片段收藏状态: $favoriteData'); final response = await post('/novel-snippets/update-favorite', data: favoriteData); return response; } catch (e) { AppLogger.e('ApiClient', '更新片段收藏状态失败', e); rethrow; } } /// 获取片段历史记录 Future getSnippetHistory(String snippetId, {int page = 0, int size = 10}) async { try { final response = await post('/novel-snippets/get-history', data: { 'snippetId': snippetId, 'page': page, 'size': size, }); return response; } catch (e) { AppLogger.e('ApiClient', '获取片段历史记录失败: snippetId=$snippetId', e); rethrow; } } /// 预览历史版本内容 Future previewSnippetHistoryVersion(String snippetId, int version) async { try { final response = await post('/novel-snippets/preview-history', data: { 'snippetId': snippetId, 'version': version, }); return response; } catch (e) { AppLogger.e('ApiClient', '预览片段历史版本失败: snippetId=$snippetId, version=$version', e); rethrow; } } /// 回退到历史版本(创建新片段) Future revertSnippetToVersion(Map revertData) async { try { AppLogger.d('ApiClient', '回退片段版本: $revertData'); final response = await post('/novel-snippets/revert-to-version', data: revertData); return response; } catch (e) { AppLogger.e('ApiClient', '回退片段版本失败', e); rethrow; } } /// 删除片段 Future deleteSnippet(String snippetId) async { try { await post('/novel-snippets/delete', data: { 'snippetId': snippetId, }); } catch (e) { AppLogger.e('ApiClient', '删除片段失败: snippetId=$snippetId', e); rethrow; } } /// 获取用户收藏的片段 Future getFavoriteSnippets({int page = 0, int size = 20}) async { try { final response = await post('/novel-snippets/get-favorites', data: { 'page': page, 'size': size, }); return response; } catch (e) { AppLogger.e('ApiClient', '获取收藏片段失败', e); rethrow; } } /// 搜索片段 Future searchSnippets(String novelId, String searchText, {int page = 0, int size = 20}) async { try { final response = await post('/novel-snippets/search', data: { 'novelId': novelId, 'searchText': searchText, 'page': page, 'size': size, }); return response; } catch (e) { AppLogger.e('ApiClient', '搜索片段失败: novelId=$novelId, searchText=$searchText', e); rethrow; } } //==== 旧的聊天相关接口 ====// /* /// 获取聊天会话列表 Future getChatSessions(String novelId) async { return post('/chats/get-by-novel', data: {'novelId': novelId}); } // ... 其他旧方法 ... */ /// 处理Dio错误 ApiException _handleDioError(DioException error) { AppLogger.e('ApiClient', 'DioException类型: ${error.type}, 请求路径: ${error.requestOptions.path}'); AppLogger.e('ApiClient', '响应状态码: ${error.response?.statusCode}'); AppLogger.e('ApiClient', '响应数据: ${error.response?.data}'); switch (error.type) { case DioExceptionType.connectionTimeout: case DioExceptionType.sendTimeout: case DioExceptionType.receiveTimeout: return ApiException(408, '请求超时,请稍后重试'); case DioExceptionType.badResponse: final statusCode = error.response?.statusCode ?? 500; // 特殊处理401错误 if (statusCode == 401) { return ApiException(401, '登录已过期,请重新登录'); } final message = _getErrorMessageFromResponse(error.response); AppLogger.w('ApiClient', '从响应中提取错误消息: $message (状态码: $statusCode)'); return ApiException(statusCode, message); case DioExceptionType.cancel: return ApiException(499, '请求被取消'); case DioExceptionType.connectionError: return ApiException(0, '网络连接失败,请检查您的网络连接'); default: return ApiException(-1, '请求失败: ${error.message}'); } } /// 从响应中获取错误信息 String _getErrorMessageFromResponse(Response? response) { if (response == null) return '未知错误'; try { final data = response.data; AppLogger.d('ApiClient', '解析错误响应数据类型: ${data.runtimeType}, 内容: $data'); if (data is Map) { // 尝试多种可能的错误字段名 String? message = data['message'] ?? data['error'] ?? data['msg'] ?? data['errorMessage'] ?? data['detail']; if (message != null && message.isNotEmpty) { AppLogger.d('ApiClient', '从响应中提取到错误消息: $message'); return message; } // 如果找不到明确的错误字段,尝试找任何包含错误信息的字段 for (final entry in data.entries) { if (entry.value is String && (entry.value as String).isNotEmpty) { AppLogger.d('ApiClient', '使用字段 ${entry.key} 作为错误消息: ${entry.value}'); return entry.value as String; } } return '请求失败'; } else if (data is String && data.isNotEmpty) { AppLogger.d('ApiClient', '直接使用字符串响应作为错误消息: $data'); return data; } final fallbackMessage = response.statusMessage ?? '未知错误'; AppLogger.d('ApiClient', '使用状态消息作为错误消息: $fallbackMessage'); return fallbackMessage; } catch (e) { AppLogger.w('ApiClient', '解析错误响应时出现异常', e); return response.statusMessage ?? '未知错误'; } } /// 关闭客户端 void dispose() { _dio.close(); } /// 获取小说的场景摘要数据(用于Plan视图) /// /// 与完整场景数据不同,只包含摘要信息,减少数据传输量 Future?> getNovelWithSceneSummaries(String novelId) async { try { final response = await _dio.post('/novels//get-with-scene-summaries', data: { 'id': novelId, }); return response.data; } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '获取小说场景摘要数据失败: $novelId', e); return null; } } /// 获取小说及其所有场景 /// /// 与分页加载不同,一次性获取小说的所有场景数据 Future?> getNovelWithAllScenes(String novelId) async { try { final response = await _dio.post('/novels/get-with-scenes', data: { 'id': novelId, }); return response.data; } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '获取小说及其所有场景失败: $novelId', e); return null; } } /// 移动场景(用于Plan视图拖拽功能) Future?> moveScene( String novelId, String sourceActId, String sourceChapterId, String sourceSceneId, String targetActId, String targetChapterId, int targetIndex, ) async { try { final data = { 'sourceActId': sourceActId, 'sourceChapterId': sourceChapterId, 'sourceSceneId': sourceSceneId, 'targetActId': targetActId, 'targetChapterId': targetChapterId, 'targetIndex': targetIndex, }; final response = await _dio.post( '/novels/$novelId/scenes/move', data: data, ); return response.data; } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '移动场景失败: $novelId', e); return null; } } /// 更新小说元数据(标题、作者、系列) Future?> updateNovelMetadata( String novelId, String title, String author, String? series ) async { try { final data = { 'title': title, 'author': author, 'series': series, }; final response = await _dio.post( '/novels/$novelId/metadata', data: data, ); return response.data; } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '更新小说元数据失败: $novelId', e); throw ApiException(-1, '更新小说元数据失败: ${e.toString()}'); } } /// 获取封面图片上传凭证 Future> getCoverUploadCredential(String novelId) async { try { final response = await _dio.post( '/novels/$novelId/cover-upload-credential', data: { 'fileName': 'cover.jpg', 'contentType': 'image/jpeg' }, ); return response.data; } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '获取封面上传凭证失败: $novelId', e); throw ApiException(-1, '获取封面上传凭证失败: ${e.toString()}'); } } /// 更新小说封面URL Future?> updateNovelCover(String novelId, String coverUrl) async { try { final data = { 'coverUrl': coverUrl, }; final response = await _dio.post( '/novels/$novelId/cover', data: data, ); return response.data; } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '更新小说封面失败: $novelId', e); throw ApiException(-1, '更新小说封面失败: ${e.toString()}'); } } /// 归档小说 Future?> archiveNovel(String novelId) async { try { final response = await _dio.post( '/novels/$novelId/archive', ); return response.data; } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '归档小说失败: $novelId', e); throw ApiException(-1, '归档小说失败: ${e.toString()}'); } } /// 删除场景 Future?> deleteScene( String novelId, String actId, String chapterId, String sceneId, ) async { try { final data = { 'novelId': novelId, 'actId': actId, 'chapterId': chapterId, 'sceneId': sceneId, }; final response = await _dio.post( '/novels/delete-scene', data: data, ); return response.data; } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '删除场景失败: $novelId', e); throw ApiException(-1, '删除场景失败: ${e.toString()}'); } } /// 删除章节 Future?> deleteChapter( String novelId, String actId, String chapterId, ) async { try { final data = { 'novelId': novelId, 'actId': actId, 'chapterId': chapterId, }; final response = await _dio.post( '/novels/delete-chapter', data: data, ); return response.data; } on DioException catch (e) { throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '删除章节失败: $novelId, $chapterId', e); throw ApiException(-1, '删除章节失败: ${e.toString()}'); } } /// 更新小说最后编辑的章节ID Future updateLastEditedChapter(String novelId, String chapterId) async { final data = { 'novelId': novelId, 'chapterId': chapterId, }; await post('/novels/update-last-edited-chapter', data: data); } /// 批量更新场景内容 Future updateScenesBatch(String novelId, List> scenes) async { final data = { 'novelId': novelId, 'scenes': scenes, }; await post('/scenes/update-batch', data: data); } /// 批量更新小说字数统计 Future updateNovelWordCounts(String novelId, Map sceneWordCounts) async { final data = { 'novelId': novelId, 'sceneWordCounts': sceneWordCounts, }; await post('/novels/update-word-counts', data: data); } /// 更新小说结构(不包含场景内容) Future updateNovelStructure(Map novelStructure) async { await post('/novels/update-structure', data: novelStructure); } /// 细粒度添加卷 - 只提供必要信息 Future> addActFine(String novelId, String title, {String? description}) async { final data = { 'novelId': novelId, 'title': title, }; if (description != null) { data['description'] = description; } return await post('/novels/add-act-fine', data: data); } /// 细粒度添加章节 - 只提供必要信息 Future> addChapterFine(String novelId, String actId, String title, {String? description}) async { final data = { 'novelId': novelId, 'actId': actId, 'title': title, }; if (description != null) { data['description'] = description; } return await post('/novels/add-chapter-fine', data: data); } /// 细粒度添加场景 - 只提供必要信息 Future> addSceneFine(String novelId, String chapterId, String title, {String? summary, int? position}) async { final data = { 'novelId': novelId, 'chapterId': chapterId, 'title': title, }; if (summary != null) { data['summary'] = summary; } if (position != null) { data['position'] = position.toString(); } return await post('/scenes/add-scene-fine', data: data); } /// 细粒度批量添加场景 - 一次添加多个场景到同一章节 Future>> addScenesBatchFine(String novelId, String chapterId, List> scenes) async { final data = { 'novelId': novelId, 'chapterId': chapterId, 'scenes': scenes, }; return await post('/novels/upsert-chapter-scenes-batch', data: data); } /// 细粒度删除卷 - 只提供ID Future deleteActFine(String novelId, String actId) async { final data = { 'novelId': novelId, 'actId': actId, }; return await post('/novels/delete-act-fine', data: data); } /// 细粒度删除章节 - 只提供ID Future deleteChapterFine(String novelId, String actId, String chapterId) async { final data = { 'novelId': novelId, 'actId': actId, 'chapterId': chapterId, }; return await post('/novels/delete-chapter-fine', data: data); } /// 细粒度删除场景 - 只提供ID Future deleteSceneFine(String sceneId) async { final data = { 'sceneId': sceneId, }; return await post('/scenes/delete-scene-fine', data: data); } Future getNovelDetailByIdText(String id) { return post('/novels/get-with-scenes-text', data: {'id': id}); } /// 通用流式处理方法,允许外部类使用 /// /// 处理字节流,解码,解析 SSE 或单行 JSON 数组,并生成指定类型的流 Stream processUniversalStream({ required Future>> byteStreamFuture, required T Function(Map) fromJson, required String logContext, }) { return _processStream( byteStreamFuture: byteStreamFuture, fromJson: fromJson, logContext: logContext, ); } /// 通用AI请求 - 流式 /// /// 发送通用AI请求并返回流式响应 Stream streamUniversalAiRequest({ required String path, required Map requestData, required T Function(Map) fromJson, }) { final byteStreamFuture = postStream(path, data: requestData); return processUniversalStream( byteStreamFuture: byteStreamFuture, fromJson: fromJson, logContext: 'streamUniversalAiRequest', ); } /// 通用AI请求 - 预览 /// /// 获取构建的提示内容,不实际发送给AI Future previewUniversalAiRequest(Map requestData) async { try { AppLogger.d('ApiClient', '发送AI预览请求'); final response = await post('/ai/universal/preview', data: requestData); if (response is Map) { return UniversalAIPreviewResponse.fromJson(response); } else { throw ApiException(-1, '预览响应格式错误'); } } catch (e) { AppLogger.e('ApiClient', '预览AI请求失败', e); rethrow; } } //==== 公共模型相关接口 ====// /// 获取公共模型列表 /// 只包含向前端暴露的安全信息,不含API Keys等敏感数据 /// 用户必须登录才能访问此接口 Future>> getPublicModels() async { try { AppLogger.d('ApiClient', '🔍 获取公共模型列表'); final response = await _dio.get('/public-models'); dynamic rawData; if (response.data is Map) { final Map responseMap = response.data; if (responseMap.containsKey('data')) { rawData = responseMap['data']; } else if (responseMap.containsKey('success') && responseMap['success'] == true) { rawData = responseMap['data'] ?? responseMap; } else { rawData = responseMap; } } else { rawData = response.data; } if (rawData is List) { AppLogger.d('ApiClient', '✅ 获取公共模型列表成功: 共${rawData.length}个模型'); return rawData.cast>(); } else { AppLogger.w('ApiClient', '❌ 公共模型列表响应格式错误: 期望List但收到${rawData.runtimeType}'); return []; } } on DioException catch (e) { AppLogger.e('ApiClient', '❌ 获取公共模型列表失败', e); throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '❌ 获取公共模型列表时发生意外错误', e); throw ApiException(-1, '获取公共模型列表失败: ${e.toString()}'); } } //==== 用户积分相关接口 ====// /// 获取当前用户的积分余额 Future> getUserCredits() async { try { AppLogger.d('ApiClient', '🔍 获取用户积分余额'); final response = await _dio.get('/credits/balance'); dynamic rawData; if (response.data is Map) { final Map responseMap = response.data; if (responseMap.containsKey('data')) { rawData = responseMap['data']; } else if (responseMap.containsKey('success') && responseMap['success'] == true) { rawData = responseMap['data'] ?? responseMap; } else { rawData = responseMap; } } else { rawData = response.data; } if (rawData is Map) { AppLogger.d('ApiClient', '✅ 获取用户积分余额成功: ${rawData['credits']}'); return rawData; } else { AppLogger.w('ApiClient', '❌ 用户积分响应格式错误: 期望Map但收到${rawData.runtimeType}'); throw ApiException(-1, '用户积分响应格式错误'); } } on DioException catch (e) { AppLogger.e('ApiClient', '❌ 获取用户积分余额失败', e); throw _handleDioError(e); } catch (e) { AppLogger.e('ApiClient', '❌ 获取用户积分余额时发生意外错误', e); throw ApiException(-1, '获取用户积分余额失败: ${e.toString()}'); } } }