Files
MaliangAINovalWriter/AINoval/lib/blocs/next_outline/next_outline_bloc.dart
2025-09-10 00:07:52 +08:00

657 lines
23 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<NextOutlineEvent, NextOutlineState> {
final NextOutlineRepository _nextOutlineRepository;
final EditorRepository _editorRepository;
final UserAIModelConfigRepository _userAIModelConfigRepository;
// 存储活跃的流订阅
final Map<String, StreamSubscription> _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<NextOutlineInitialized>(_onInitialized);
on<LoadChaptersRequested>(_onLoadChaptersRequested);
on<LoadAIModelConfigsRequested>(_onLoadAIModelConfigsRequested);
on<UpdateChapterRangeRequested>(_onUpdateChapterRangeRequested);
on<GenerateNextOutlinesRequested>(_onGenerateNextOutlinesRequested);
on<RegenerateAllOutlinesRequested>(_onRegenerateAllOutlinesRequested);
on<RegenerateSingleOutlineRequested>(_onRegenerateSingleOutlineRequested);
on<OutlineSelected>(_onOutlineSelected);
on<SaveSelectedOutlineRequested>(_onSaveSelectedOutlineRequested);
on<OutlineGenerationChunkReceived>(_onOutlineGenerationChunkReceived);
on<GenerationErrorOccurred>(_onGenerationErrorOccurred);
}
/// 初始化
Future<void> _onInitialized(
NextOutlineInitialized event,
Emitter<NextOutlineState> emit,
) async {
emit(NextOutlineState.initial(novelId: event.novelId));
// 加载章节和AI模型配置
add(LoadChaptersRequested(novelId: event.novelId));
add(const LoadAIModelConfigsRequested());
}
/// 加载章节列表
Future<void> _onLoadChaptersRequested(
LoadChaptersRequested event,
Emitter<NextOutlineState> emit,
) async {
try {
emit(state.copyWith(
generationStatus: GenerationStatus.loadingChapters,
clearError: true,
));
// 获取小说数据,从中提取章节列表
final novel = await _editorRepository.getNovel(event.novelId);
List<novel_models.Chapter> 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<void> _onLoadAIModelConfigsRequested(
LoadAIModelConfigsRequested event,
Emitter<NextOutlineState> 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<NextOutlineState> 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<void> _onGenerateNextOutlinesRequested(
GenerateNextOutlinesRequested event,
Emitter<NextOutlineState> 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<String>? 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<void> _onRegenerateAllOutlinesRequested(
RegenerateAllOutlinesRequested event,
Emitter<NextOutlineState> 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<void> _onRegenerateSingleOutlineRequested(
RegenerateSingleOutlineRequested event,
Emitter<NextOutlineState> 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<OutlineOptionState>.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<OutlineOptionState>.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<NextOutlineState> 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<void> _onSaveSelectedOutlineRequested(
SaveSelectedOutlineRequested event,
Emitter<NextOutlineState> 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<NextOutlineState> emit,
) {
try {
final List<OutlineOptionState> 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<NextOutlineState> 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<NextOutlineState> 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<void> close() {
_cancelAllSubscriptions();
return super.close();
}
}