import 'dart:async'; import 'package:ainoval/blocs/next_outline/next_outline_event.dart'; import 'package:ainoval/blocs/next_outline/next_outline_state.dart'; import 'package:ainoval/models/next_outline/next_outline_dto.dart'; import 'package:ainoval/models/next_outline/outline_generation_chunk.dart'; import 'package:ainoval/models/novel_structure.dart' as novel_models; import 'package:ainoval/services/api_service/base/api_exception.dart'; import 'package:ainoval/services/api_service/repositories/editor_repository.dart'; import 'package:ainoval/services/api_service/repositories/next_outline_repository.dart'; import 'package:ainoval/services/api_service/repositories/user_ai_model_config_repository.dart'; import 'package:ainoval/utils/logger.dart'; import 'package:ainoval/utils/event_bus.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ainoval/config/app_config.dart'; /// 剧情推演BLoC class NextOutlineBloc extends Bloc { final NextOutlineRepository _nextOutlineRepository; final EditorRepository _editorRepository; final UserAIModelConfigRepository _userAIModelConfigRepository; // 存储活跃的流订阅 final Map _activeSubscriptions = {}; final String _tag = 'NextOutlineBloc'; NextOutlineBloc({ required NextOutlineRepository nextOutlineRepository, required EditorRepository editorRepository, required UserAIModelConfigRepository userAIModelConfigRepository, }) : _nextOutlineRepository = nextOutlineRepository, _editorRepository = editorRepository, _userAIModelConfigRepository = userAIModelConfigRepository, super(NextOutlineState.initial(novelId: '')) { on(_onInitialized); on(_onLoadChaptersRequested); on(_onLoadAIModelConfigsRequested); on(_onUpdateChapterRangeRequested); on(_onGenerateNextOutlinesRequested); on(_onRegenerateAllOutlinesRequested); on(_onRegenerateSingleOutlineRequested); on(_onOutlineSelected); on(_onSaveSelectedOutlineRequested); on(_onOutlineGenerationChunkReceived); on(_onGenerationErrorOccurred); } /// 初始化 Future _onInitialized( NextOutlineInitialized event, Emitter emit, ) async { emit(NextOutlineState.initial(novelId: event.novelId)); // 加载章节和AI模型配置 add(LoadChaptersRequested(novelId: event.novelId)); add(const LoadAIModelConfigsRequested()); } /// 加载章节列表 Future _onLoadChaptersRequested( LoadChaptersRequested event, Emitter emit, ) async { try { emit(state.copyWith( generationStatus: GenerationStatus.loadingChapters, clearError: true, )); // 获取小说数据,从中提取章节列表 final novel = await _editorRepository.getNovel(event.novelId); List chapters = []; String? startChapterId; String? endChapterId; if (novel != null) { // 提取所有章节 for (final act in novel.acts) { chapters.addAll(act.chapters); } } // 默认范围:从第一章到最后一章(用于剧情推演的上下文) if (chapters.isNotEmpty) { startChapterId = chapters.first.id; endChapterId = chapters.last.id; AppLogger.i(_tag, '设置默认章节范围: 从第一章(${chapters.first.title}) 到最后一章(${chapters.last.title})'); } emit(state.copyWith( chapters: chapters, startChapterId: startChapterId, endChapterId: endChapterId, generationStatus: GenerationStatus.idle, )); } catch (e) { AppLogger.e(_tag, '加载章节失败', e); emit(state.copyWith( generationStatus: GenerationStatus.error, errorMessage: '加载章节失败: $e', )); } } /// 加载AI模型配置 Future _onLoadAIModelConfigsRequested( LoadAIModelConfigsRequested event, Emitter emit, ) async { try { emit(state.copyWith( generationStatus: GenerationStatus.loadingModels, )); // 从AppConfig获取当前用户ID,而不是使用硬编码的"current" final String userId = AppConfig.userId ?? ''; final configs = await _userAIModelConfigRepository.listConfigurations(userId: userId); emit(state.copyWith( aiModelConfigs: configs, generationStatus: GenerationStatus.idle, clearError: true, )); } catch (e) { AppLogger.e(_tag, '加载AI模型配置失败', e); // 不进入错误状态,而是使用空配置列表继续 emit(state.copyWith( aiModelConfigs: [], // 使用空配置列表 generationStatus: GenerationStatus.idle, // 改为idle状态而不是error clearError: true, // 清除错误 )); AppLogger.w(_tag, '使用空AI模型配置列表继续,生成时将使用后端默认配置'); } } /// 更新上下文章节范围 void _onUpdateChapterRangeRequested( UpdateChapterRangeRequested event, Emitter emit, ) { // 验证章节顺序 String? errorMessage; if (event.startChapterId != null && event.endChapterId != null && state.chapters.isNotEmpty) { // 查找章节索引 int? startIndex; int? endIndex; for (int i = 0; i < state.chapters.length; i++) { if (state.chapters[i].id == event.startChapterId) { startIndex = i; } if (state.chapters[i].id == event.endChapterId) { endIndex = i; } // 如果两个索引都找到了,可以提前结束循环 if (startIndex != null && endIndex != null) { break; } } // 检查有效性 if (startIndex != null && endIndex != null && startIndex > endIndex) { errorMessage = '起始章节不能晚于结束章节'; AppLogger.w(_tag, errorMessage); } } emit(state.copyWith( startChapterId: event.startChapterId, endChapterId: event.endChapterId, errorMessage: errorMessage, clearError: errorMessage == null, )); } /// 生成剧情大纲 Future _onGenerateNextOutlinesRequested( GenerateNextOutlinesRequested event, Emitter emit, ) async { try { // 取消所有活跃的流订阅 _cancelAllSubscriptions(); // 处理章节范围:如果没有提供startChapterId,使用第一章 String? finalStartChapterId = event.request.startChapterId; String? finalEndChapterId = event.request.endChapterId; if (finalStartChapterId == null && state.chapters.isNotEmpty) { finalStartChapterId = state.chapters.first.id; AppLogger.i(_tag, '未提供startChapterId,使用第一章: ${state.chapters.first.title}'); } if (finalEndChapterId == null && state.chapters.isNotEmpty) { finalEndChapterId = state.chapters.last.id; AppLogger.i(_tag, '未提供endChapterId,使用最后一章: ${state.chapters.last.title}'); } // 处理默认AI配置:如果没有提供selectedConfigIds,使用前3个可用配置 List? finalConfigIds = event.request.selectedConfigIds; if (finalConfigIds == null || finalConfigIds.isEmpty) { if (state.aiModelConfigs.isNotEmpty) { final configCount = state.aiModelConfigs.length; final useCount = configCount >= event.request.numOptions ? event.request.numOptions : configCount; finalConfigIds = state.aiModelConfigs .take(useCount) .map((config) => config.id) .toList(); AppLogger.i(_tag, '使用默认AI配置: ${finalConfigIds.join(", ")}'); } else { // 如果没有可用的AI配置,使用null让后端选择默认配置 finalConfigIds = null; AppLogger.w(_tag, '没有可用的AI配置,使用null让后端选择默认配置'); } } // 创建修正后的请求 final correctedRequest = GenerateNextOutlinesRequest( startChapterId: finalStartChapterId, endChapterId: finalEndChapterId, numOptions: event.request.numOptions, authorGuidance: event.request.authorGuidance, selectedConfigIds: finalConfigIds, regenerateHint: event.request.regenerateHint, ); emit(state.copyWith( generationStatus: GenerationStatus.generatingInitial, outlineOptions: [], clearSelectedOption: true, clearError: true, numOptions: correctedRequest.numOptions, authorGuidance: correctedRequest.authorGuidance, )); AppLogger.i(_tag, '开始生成剧情大纲: startChapter=${correctedRequest.startChapterId}, endChapter=${correctedRequest.endChapterId}, numOptions=${correctedRequest.numOptions}, configs=${finalConfigIds?.join(", ")}'); // 订阅流式响应 final stream = _nextOutlineRepository.generateNextOutlinesStream( state.novelId, correctedRequest, ); final subscription = stream.listen( (chunk) { // 处理接收到的块 add(OutlineGenerationChunkReceived( optionId: chunk.optionId, optionTitle: chunk.optionTitle, textChunk: chunk.textChunk, isFinalChunk: chunk.isFinalChunk, error: chunk.error, )); }, onError: (error) { AppLogger.e(_tag, '生成剧情大纲流错误', error); String errorMessage = error.toString(); if (error is ApiException) { errorMessage = error.message; } // 不再尝试关联特定选项,直接触发全局错误处理 add(GenerationErrorOccurred(error: errorMessage)); }, onDone: () { AppLogger.i(_tag, '生成剧情大纲流完成'); // 检查是否所有选项都已完成 _checkAllOptionsComplete(emit); }, ); // 存储订阅 _activeSubscriptions['generate'] = subscription; } catch (e) { AppLogger.e(_tag, '生成剧情大纲失败', e); emit(state.copyWith( generationStatus: GenerationStatus.error, errorMessage: '生成剧情大纲失败: $e', )); } } /// 重新生成全部剧情大纲 Future _onRegenerateAllOutlinesRequested( RegenerateAllOutlinesRequested event, Emitter emit, ) async { try { // 构建重新生成请求 final request = GenerateNextOutlinesRequest( startChapterId: state.startChapterId, endChapterId: state.endChapterId, numOptions: state.numOptions, authorGuidance: state.authorGuidance, regenerateHint: event.regenerateHint, selectedConfigIds: state.aiModelConfigs.isNotEmpty ? state.aiModelConfigs .take(state.numOptions) .map((config) => config.id) .toList() : null, ); // 调用生成事件 add(GenerateNextOutlinesRequested(request: request)); } catch (e) { AppLogger.e(_tag, '重新生成所有剧情大纲失败', e); emit(state.copyWith( generationStatus: GenerationStatus.error, errorMessage: '重新生成所有剧情大纲失败: $e', )); } } /// 重新生成单个剧情大纲 Future _onRegenerateSingleOutlineRequested( RegenerateSingleOutlineRequested event, Emitter emit, ) async { try { // 找到要重新生成的选项 final optionIndex = state.outlineOptions .indexWhere((option) => option.optionId == event.request.optionId); if (optionIndex == -1) { throw Exception('未找到指定的剧情选项'); } // 取消该选项的现有订阅 final subKey = 'regenerate_${event.request.optionId}'; if (_activeSubscriptions.containsKey(subKey)) { _activeSubscriptions[subKey]?.cancel(); _activeSubscriptions.remove(subKey); } // 更新选项状态为生成中 final updatedOptions = List.from(state.outlineOptions); updatedOptions[optionIndex] = updatedOptions[optionIndex].copyWith( isGenerating: true, isComplete: false, ); emit(state.copyWith( outlineOptions: updatedOptions, generationStatus: GenerationStatus.generatingSingle, clearError: true, )); // 订阅流式响应 final stream = _nextOutlineRepository.regenerateOutlineOption( state.novelId, event.request, ); final subscription = stream.listen( (chunk) { // 处理接收到的块 add(OutlineGenerationChunkReceived( optionId: chunk.optionId, optionTitle: chunk.optionTitle, textChunk: chunk.textChunk, isFinalChunk: chunk.isFinalChunk, error: chunk.error, )); }, onError: (error) { AppLogger.e(_tag, '重新生成单个剧情大纲流错误', error); String errorMessage = error.toString(); if (error is ApiException) { errorMessage = error.message; } // 更新对应选项的错误状态,而不是全局错误 final errorOptionIndex = state.outlineOptions .indexWhere((option) => option.optionId == event.request.optionId); if (errorOptionIndex != -1) { final updatedErrorOptions = List.from(state.outlineOptions); updatedErrorOptions[errorOptionIndex] = updatedErrorOptions[errorOptionIndex].copyWith( isGenerating: false, isComplete: true, errorMessage: errorMessage, ); emit(state.copyWith( outlineOptions: updatedErrorOptions, )); _checkAllOptionsComplete(emit); } else { // 如果找不到选项,回退到全局错误 add(GenerationErrorOccurred(error: errorMessage)); } }, onDone: () { AppLogger.i(_tag, '重新生成单个剧情大纲流完成'); // 检查是否所有选项都已完成 _checkAllOptionsComplete(emit); }, ); // 存储订阅 _activeSubscriptions[subKey] = subscription; } catch (e) { AppLogger.e(_tag, '重新生成单个剧情大纲失败', e); emit(state.copyWith( generationStatus: GenerationStatus.error, errorMessage: '重新生成单个剧情大纲失败: $e', )); } } /// 选择剧情大纲 void _onOutlineSelected( OutlineSelected event, Emitter emit, ) { // 获取选择的选项索引 final optionIndex = state.outlineOptions.indexWhere((option) => option.optionId == event.optionId); // 如果找到选项且outputGeneration存在 if (optionIndex != -1 && state.outputGeneration != null) { // 创建新的outputGeneration,更新selectedOutlineIndex final updatedOutputGeneration = NextOutlineOutput( outlineList: state.outputGeneration!.outlineList, generationTimeMs: state.outputGeneration!.generationTimeMs, selectedOutlineIndex: optionIndex, ); emit(state.copyWith( selectedOptionId: event.optionId, outputGeneration: updatedOutputGeneration, clearError: true, )); } else { // 仅更新选项ID emit(state.copyWith( selectedOptionId: event.optionId, clearError: true, )); } } /// 保存选中的剧情大纲 Future _onSaveSelectedOutlineRequested( SaveSelectedOutlineRequested event, Emitter emit, ) async { try { // 设置状态为保存中 emit(state.copyWith( generationStatus: GenerationStatus.saving, clearError: true, )); // 检查是否有选中的大纲索引 int? outlineIndex = event.selectedOutlineIndex; // 如果没有传入索引,但有选中的选项ID,则尝试查找对应的索引 if (outlineIndex == null && state.selectedOptionId != null) { AppLogger.i(_tag, '尝试使用selectedOptionId查找大纲索引'); final selectedOptionIndex = state.outlineOptions.indexWhere( (option) => option.optionId == state.selectedOptionId ); if (selectedOptionIndex != -1) { outlineIndex = selectedOptionIndex; AppLogger.i(_tag, '已找到对应索引: $outlineIndex'); } } // 检查输出生成结果和索引是否有效 if (outlineIndex == null || outlineIndex < 0 || state.outputGeneration == null || outlineIndex >= state.outputGeneration!.outlineList.length) { final errorMsg = '未选择有效的大纲或大纲不存在: index=$outlineIndex, outputGeneration=${state.outputGeneration != null}, selectedOptionId=${state.selectedOptionId}'; AppLogger.e(_tag, errorMsg); emit(state.copyWith( generationStatus: GenerationStatus.error, errorMessage: errorMsg, )); return; } var selectedOutline = state.outputGeneration!.outlineList[outlineIndex]; AppLogger.i(_tag, '正在保存大纲: ${selectedOutline.title}'); // 调用保存API final response = await _nextOutlineRepository.saveNextOutline( state.novelId, event.request, ); // 保存成功 AppLogger.i(_tag, '剧情大纲保存成功'); // 发送小说结构更新事件 EventBus.instance.fire(NovelStructureUpdatedEvent( novelId: state.novelId, updateType: 'outline_saved', data: { 'outlineId': event.request.outlineId, 'insertType': event.request.insertType, 'newChapterId': response.newChapterId, 'newSceneId': response.newSceneId, 'targetChapterId': response.targetChapterId, 'outline': selectedOutline.toJson(), 'apiResult': response.toJson(), }, )); // 保持状态不变,只更新生成状态为空闲 emit(state.copyWith( generationStatus: GenerationStatus.idle, // 不更改其他状态,保留当前大纲和选项 )); } catch (e) { AppLogger.e(_tag, '保存剧情大纲失败', e); emit(state.copyWith( generationStatus: GenerationStatus.error, errorMessage: '保存剧情大纲失败: $e', )); } } /// 处理生成块接收事件 void _onOutlineGenerationChunkReceived( OutlineGenerationChunkReceived event, Emitter emit, ) { try { final List currentOptions = List.from(state.outlineOptions); int optionIndex = currentOptions.indexWhere((option) => option.optionId == event.optionId); OutlineOptionState updatedOption; if (optionIndex == -1) { // ---- 新增:动态创建新的选项状态 ---- AppLogger.i(_tag, '首次接收到选项 ${event.optionId} 的数据块,创建新的状态'); updatedOption = OutlineOptionState( optionId: event.optionId, title: event.optionTitle, content: event.textChunk, isGenerating: !event.isFinalChunk, isComplete: event.isFinalChunk, errorMessage: event.error, // 处理可能直接在chunk中传来的错误 ); currentOptions.add(updatedOption); // ------------------------------- } else { // ---- 更新现有选项状态 ---- final existingOption = currentOptions[optionIndex]; updatedOption = existingOption.copyWith( // 追加内容 content: existingOption.content + event.textChunk, // 更新标题(如果新的标题非空且不同) title: (event.optionTitle != null && event.optionTitle!.isNotEmpty && event.optionTitle != existingOption.title) ? event.optionTitle : existingOption.title, // 更新状态 isGenerating: !event.isFinalChunk, isComplete: event.isFinalChunk, // 更新错误信息(如果新的错误信息非空) errorMessage: event.error ?? existingOption.errorMessage, ); currentOptions[optionIndex] = updatedOption; // ------------------------ } emit(state.copyWith(outlineOptions: currentOptions)); // 检查是否所有选项都已完成 (可以在这里检查,或者依赖 onDone) if (currentOptions.every((o) => o.isComplete)) { _checkAllOptionsComplete(emit); } } catch (e, stackTrace) { AppLogger.e(_tag, '处理生成块失败 for ${event.optionId}', e, stackTrace); // 考虑是否要将此错误设置到对应的option上或触发全局错误 // 为了避免影响其他流,暂时只记录日志 } } /// 处理生成错误事件 void _onGenerationErrorOccurred( GenerationErrorOccurred event, Emitter emit, ) { AppLogger.e(_tag, '全局生成错误: ${event.error}'); // 停止所有仍在进行的生成,并标记错误 final updatedOptions = state.outlineOptions.map((option) { if (option.isGenerating) { // 只处理还在生成中的选项 return option.copyWith( isGenerating: false, isComplete: true, // 标记为完成(即使是失败) errorMessage: event.error, ); } return option; // 其他选项保持不变 }).toList(); emit(state.copyWith( generationStatus: GenerationStatus.error, // 设置全局状态为错误 errorMessage: event.error, outlineOptions: updatedOptions, // 更新选项列表 )); } /// 检查所有选项是否已完成生成 void _checkAllOptionsComplete(Emitter emit) { if (state.outlineOptions.every((option) => option.isComplete)) { // 所有选项都已完成生成 // 将outlineOptions转换为NextOutlineOutput final outlineList = state.outlineOptions.map((option) => NextOutlineDTO( id: option.optionId, title: option.title ?? 'Untitled Outline', content: option.content, configId: option.configId, )).toList(); final outputGeneration = NextOutlineOutput( outlineList: outlineList, generationTimeMs: DateTime.now().millisecondsSinceEpoch, selectedOutlineIndex: null, // 初始时没有选中的大纲 ); // 更新状态,设置status为success emit(state.copyWith( generationStatus: GenerationStatus.idle, status: NextOutlineStatus.success, outputGeneration: outputGeneration, )); } } /// 取消所有活跃的流订阅 void _cancelAllSubscriptions() { _activeSubscriptions.forEach((key, subscription) { subscription.cancel(); }); _activeSubscriptions.clear(); } @override Future close() { _cancelAllSubscriptions(); return super.close(); } }