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

3458 lines
129 KiB
Dart
Raw 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 '../../models/compose_preview.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../config/app_config.dart';
import '../../models/setting_generation_session.dart';
import '../../models/setting_node.dart';
import '../../models/setting_type.dart';
import '../../models/setting_generation_event.dart' as event_model;
import '../../models/strategy_template_info.dart';
import '../../services/api_service/repositories/setting_generation_repository.dart';
import '../../models/ai_request_models.dart';
import '../../utils/logger.dart';
import '../../utils/setting_node_utils.dart';
import 'setting_generation_event.dart';
import 'setting_generation_state.dart';
/// 设定生成BLoC
///
/// 核心业务逻辑:
/// 1. 支持用户维度的历史记录管理,不再依赖特定小说
/// 2. 提供两种编辑模式:创建新快照 vs 编辑上次设定
/// 3. 支持从历史记录创建编辑会话
/// 4. 实现流式节点渲染,提供良好的用户体验
/// 5. 支持跨小说的设定复用和恢复
class SettingGenerationBloc extends Bloc<SettingGenerationBlocEvent, SettingGenerationState> {
final SettingGenerationRepository _repository;
final String _tag = 'SettingGenerationBloc';
StreamSubscription? _generationStreamSubscription;
StreamSubscription? _updateStreamSubscription;
StreamSubscription? _composeStreamSubscription; // 新增:写作编排流
Timer? _highlightRemovalTimer;
Timer? _renderProcessTimer; // 新增:用于处理渲染队列的定时器
Timer? _timeoutTimer; // 新增:用于处理业务超时的定时器(基于最后活动时间的滑动窗口)
DateTime? _lastActivityAt; // 新增:记录最后一次收到生成/进度事件的时间
final Duration _timeoutDuration = const Duration(minutes: 5); // 统一超时时长调整为5分钟
SettingGenerationBloc({
required SettingGenerationRepository repository,
}) : _repository = repository,
super(const SettingGenerationInitial()) {
on<LoadStrategiesEvent>(_onLoadStrategies);
on<LoadHistoriesEvent>(_onLoadHistories);
on<StartSessionFromNovelEvent>(_onStartSessionFromNovel);
on<StartGenerationEvent>(_onStartGeneration);
on<AdjustGenerationEvent>(_onAdjustGeneration);
on<UpdateNodeEvent>(_onUpdateNode);
on<SelectNodeEvent>(_onSelectNode);
on<ToggleViewModeEvent>(_onToggleViewMode);
on<ApplyPendingChangesEvent>(_onApplyPendingChanges);
on<CancelPendingChangesEvent>(_onCancelPendingChanges);
on<UndoNodeChangeEvent>(_onUndoNodeChange);
on<SaveGeneratedSettingsEvent>(_onSaveGeneratedSettings);
on<CreateNewSessionEvent>(_onCreateNewSession);
on<SelectSessionEvent>(_onSelectSession);
on<CreateSessionFromHistoryEvent>(_onLoadHistoryDetail);
on<UpdateAdjustmentPromptEvent>(_onUpdateAdjustmentPrompt);
on<GetSessionStatusEvent>(_onGetSessionStatus);
on<CancelSessionEvent>(_onCancelSession);
on<GetUserHistoriesEvent>(_onGetUserHistories);
on<DeleteHistoryEvent>(_onDeleteHistory);
on<CopyHistoryEvent>(_onCopyHistory);
on<RestoreHistoryToNovelEvent>(_onRestoreHistoryToNovel);
on<ResetEvent>(_onReset);
on<RetryEvent>(_onRetry);
// NOVEL_COMPOSE 事件族
on<StartComposeOutlineEvent>(_onStartComposeOutline);
on<StartComposeChaptersEvent>(_onStartComposeChapters);
on<StartComposeBundleEvent>(_onStartComposeBundle);
on<RefineComposeEvent>(_onRefineCompose);
on<CancelComposeEvent>(_onCancelCompose);
on<_HandleGenerationEventInternal>(_onHandleGenerationEvent);
on<_HandleGenerationErrorInternal>(_onHandleGenerationError);
on<_HandleGenerationCompleteInternal>(_onHandleGenerationComplete);
on<_ProcessPendingNodes>(_onProcessPendingNodes);
on<_TimeoutCheckInternal>(_onTimeoutCheckInternal);
// 新增:渲染相关事件处理器
on<StartNodeRenderEvent>(_onStartNodeRender);
on<CompleteNodeRenderEvent>(_onCompleteNodeRender);
on<ProcessRenderQueueEvent>(_onProcessRenderQueue);
// 新增:内容更新事件处理器
on<UpdateNodeContentEvent>(_onUpdateNodeContent);
// 移除:不再需要的复杂保存节点设定逻辑
// on<SaveNodeSettingEvent>(_onSaveNodeSetting);
// on<ConfirmCreateHistoryAndSaveNodeEvent>(_onConfirmCreateHistoryAndSaveNode);
}
@override
Future<void> close() {
_generationStreamSubscription?.cancel();
_updateStreamSubscription?.cancel();
_composeStreamSubscription?.cancel();
_highlightRemovalTimer?.cancel();
_renderProcessTimer?.cancel(); // 新增:清理渲染处理定时器
_timeoutTimer?.cancel(); // 新增:清理超时定时器
return super.close();
}
// ============== 超时相关工具方法(按最后活动时间计算) ==============
void _resetInactivityTimeout() {
// 每次重置都会取消旧定时器并设置新定时器,触发时仅派发内部事件
_timeoutTimer?.cancel();
_timeoutTimer = Timer(_timeoutDuration, () {
add(const _TimeoutCheckInternal());
});
}
// ==================== NOVEL_COMPOSE 处理器 ====================
// ===== 写作编排黄金三章等UI预览数据通道 =====
final _composePreviewController = StreamController<List<ComposeChapterPreview>>.broadcast();
final _composeGeneratingController = StreamController<bool>.broadcast();
// 新增:写作可开始状态(绑定完成后置为可用)
final _composeReadyController = StreamController<ComposeReadyInfo>.broadcast();
String _composeMode = '';
int _composeExpectedChapters = 0;
final StringBuffer _composeBuffer = StringBuffer();
List<ComposeChapterPreview> _composePreview = [];
Stream<List<ComposeChapterPreview>> get composePreviewStream => _composePreviewController.stream;
Stream<bool> get composeGeneratingStream => _composeGeneratingController.stream;
Stream<ComposeReadyInfo> get composeReadyStream => _composeReadyController.stream;
void _resetComposePreview(String mode, int chapterCount) {
_composeMode = mode;
_composeExpectedChapters = chapterCount;
_composeBuffer.clear();
_composePreview = List.generate(chapterCount, (i) => ComposeChapterPreview(index: i + 1));
_composePreviewController.add(List.unmodifiable(_composePreview));
}
void _publishComposeGenerating(bool v) {
_composeGeneratingController.add(v);
}
void _handleComposeChunk(UniversalAIResponse resp) {
// 完成信号仅以finishReason为准避免将仅含metadata的分片误判为完成
if (resp.finishReason != null && resp.finishReason!.isNotEmpty) {
_publishComposeGenerating(false);
return;
}
// 新增:处理后端发来的绑定信号(保存完成后将 novelId 与 session 绑定)
try {
if (resp.metadata.containsKey('composeBind')) {
final dynamic bind = resp.metadata['composeBind'];
String sessionId = '';
String novelId = '';
if (bind is Map) {
sessionId = (bind['sessionId'] ?? '').toString();
novelId = (bind['novelId'] ?? '').toString();
if (sessionId.isNotEmpty && novelId.isNotEmpty) {
_updateSessionNovelId(sessionId, novelId);
}
}
// 推送可开始状态(若有)
bool? ready;
String reason = '';
if (resp.metadata.containsKey('composeReady')) {
final r = resp.metadata['composeReady'];
ready = (r is bool) ? r : (r is String ? (r.toLowerCase() == 'true') : null);
}
if (resp.metadata.containsKey('composeReadyReason')) {
reason = (resp.metadata['composeReadyReason'] ?? '').toString();
}
try {
AppLogger.i(_tag, 'ComposeBind: sessionId=' + sessionId + ', novelId=' + novelId + ', ready=' + ((ready == null) ? 'null' : ready.toString()) + ', reason=' + (reason.isEmpty ? 'none' : reason));
} catch (_) {}
if (ready != null) {
_composeReadyController.add(ComposeReadyInfo(
ready: ready,
reason: reason,
novelId: novelId,
sessionId: sessionId,
));
}
}
} catch (_) {}
// 优先后端提供的结构化大纲metadata.composeOutlines
try {
if (resp.metadata.containsKey('composeOutlines') && resp.metadata['composeOutlines'] is List) {
final List<dynamic> arr = resp.metadata['composeOutlines'] as List<dynamic>;
final previews = <ComposeChapterPreview>[];
for (final item in arr) {
if (item is Map) {
final idx = (item['index'] is int) ? item['index'] as int : int.tryParse('${item['index']}') ?? (previews.length + 1);
final title = (item['title'] ?? '').toString();
final summary = (item['summary'] ?? '').toString();
previews.add(ComposeChapterPreview(index: idx, title: title, outline: summary));
}
}
if (previews.isNotEmpty) {
// 保持原有模式outline_plus_chapters/chapters仅更新章节预计数量与预览内容
_composeExpectedChapters = previews.length;
_composePreview = previews;
_composePreviewController.add(List.unmodifiable(_composePreview));
return; // 已消费此分片
}
}
} catch (_) {}
if (resp.content.isEmpty) return;
_composeBuffer.write(resp.content);
//调试日志:分片与模式
// try {
// AppLogger.d(_tag, '[Compose] chunk received, mode=$_composeMode, chunkLen=${resp.content.length}, bufferLen=${_composeBuffer.length}');
// } catch (_) {}
final buffer = _composeBuffer.toString();
if (_composeMode == 'outline') {
_composePreview = _parseOutlineToPreview(buffer, _composeExpectedChapters);
_composePreviewController.add(List.unmodifiable(_composePreview));
} else {
// 仅当出现章节标签时再解析避免用纯大纲文本覆盖已通过metadata构建的预览
final hasChapterTags = RegExp(r"\[CHAPTER_\d+_(?:OUTLINE|CONTENT)\]").hasMatch(buffer);
if (hasChapterTags) {
_composePreview = _parseChaptersToPreview(buffer, _composeExpectedChapters);
_composePreviewController.add(List.unmodifiable(_composePreview));
}
}
// 调试日志:解析后预览摘要
// try {
// final first = _composePreview.isNotEmpty ? _composePreview.first : null;
// AppLogger.d(_tag, '[Compose] preview updated: count=${_composePreview.length}, firstTitle=${first?.title}, firstOutlineLen=${first?.outline.length ?? 0}, firstContentLen=${first?.content.length ?? 0}');
// } catch (_) {}
}
List<ComposeChapterPreview> _parseOutlineToPreview(String text, int expected) {
final List<ComposeChapterPreview> list = List.generate(expected, (i) => ComposeChapterPreview(index: i + 1));
// 块级解析:一个 [OUTLINE_ITEM ...] 开始,直到下一个 [OUTLINE_ITEM ...] 之前的所有内容归为同一大纲块
final tag = RegExp(r"\[OUTLINE_ITEM[^\]]*\]");
final tags = tag.allMatches(text).toList();
// 将标签前的前导内容并入第1项避免丢失模型在第一个标签前输出的文字
if (tags.isNotEmpty && tags.first.start > 0 && expected > 0) {
final prefix = text.substring(0, tags.first.start).trim();
if (prefix.isNotEmpty) {
final mergedTitle = _extractTitle(prefix);
list[0] = list[0].copyWith(title: mergedTitle, outline: prefix);
}
}
int filled = 0;
for (int t = 0; t < tags.length && filled < expected; t++) {
final start = tags[t].start;
final end = (t + 1 < tags.length) ? tags[t + 1].start : text.length;
String block = text.substring(start, end).trim();
if (block.isEmpty) continue;
// 移除块内首个 [OUTLINE_ITEM ...] 标签,仅保留正文
block = block.replaceFirst(tag, '').trim();
final title = _extractTitle(block);
list[filled] = list[filled].copyWith(title: title, outline: block);
filled++;
}
// 回退:若未匹配到任何带标记的大纲,则按空行分段
if (filled == 0) {
final blocks = text.split(RegExp(r"\n\n+"));
for (final b in blocks) {
final t = b.trim();
if (t.isEmpty) continue;
if (filled >= expected) break;
list[filled] = list[filled].copyWith(title: _extractTitle(t), outline: t);
filled++;
}
}
return list;
}
List<ComposeChapterPreview> _parseChaptersToPreview(String text, int expected) {
final List<ComposeChapterPreview> list = List.generate(expected, (i) => ComposeChapterPreview(index: i + 1));
final outlineTag = RegExp(r"\[CHAPTER_(\d+)_OUTLINE\]");
final contentTag = RegExp(r"\[CHAPTER_(\d+)_CONTENT\]");
// 找到所有标签位置(兼容 OUTLINE_ITEM
final tagPattern = RegExp(r"\[(?:\s*OUTLINE\s*_ITEM[^\]]+|CHAPTER_\d+_OUTLINE|CHAPTER_\d+_CONTENT)\]");
final tags = tagPattern.allMatches(text).toList();
// 前置无标签片段并入第1章大纲避免丢失信息
if (tags.isNotEmpty && tags.first.start > 0 && expected > 0) {
final prefix = text.substring(0, tags.first.start).trim();
if (prefix.isNotEmpty) {
final old = list[0];
final mergedOutline = (old.outline.isEmpty ? '' : old.outline + "\n") + prefix;
list[0] = old.copyWith(outline: mergedOutline);
}
}
for (int t = 0; t < tags.length; t++) {
final match = tags[t];
final tagText = text.substring(match.start, match.end);
final start = match.end;
final end = (t + 1 < tags.length) ? tags[t + 1].start : text.length;
final segment = text.substring(start, end).trim();
final outlineM = outlineTag.firstMatch(tagText);
final contentM = contentTag.firstMatch(tagText);
if (outlineM != null) {
final idx = int.tryParse(outlineM.group(1) ?? '') ?? 0;
if (idx >= 1 && idx <= expected) {
final old = list[idx - 1];
list[idx - 1] = old.copyWith(title: _extractTitle(segment), outline: segment);
}
continue;
}
if (contentM != null) {
final idx = int.tryParse(contentM.group(1) ?? '') ?? 0;
if (idx >= 1 && idx <= expected) {
final old = list[idx - 1];
list[idx - 1] = old.copyWith(content: segment);
}
continue;
}
// 兼容:当仍输出 [OUTLINE_ITEM ...] 时,按顺序或 index= 提示填充
if (RegExp(r"OUTLINE\s*_ITEM", caseSensitive: false).hasMatch(tagText)) {
int? idx;
final m = RegExp(r"index\s*=\s*(\d+)", caseSensitive: false).firstMatch(tagText);
if (m != null) idx = int.tryParse(m.group(1) ?? '');
if (idx != null && idx >= 1 && idx <= expected) {
final old = list[idx - 1];
final title = _extractTitle(segment);
list[idx - 1] = old.copyWith(title: title, outline: segment);
} else {
for (int i = 0; i < expected; i++) {
if (list[i].outline.isEmpty) {
final old = list[i];
final title = _extractTitle(segment);
list[i] = old.copyWith(title: title, outline: segment);
break;
}
}
}
}
}
return list;
}
String _extractTitle(String text) {
// 简易提取:匹配 "标题xxx" 或第一行前20字
final m = RegExp(r"标题[:]\s*([^\n]{2,40})").firstMatch(text);
if (m != null) return m.group(1)!.trim();
final firstLine = text.split('\n').first.trim();
return firstLine.length > 20 ? firstLine.substring(0, 20) : firstLine;
}
void _onStartComposeOutline(
StartComposeOutlineEvent event,
Emitter<SettingGenerationState> emit,
) {
final composeParams = {
'mode': 'outline',
'chapterCount': event.chapterCount,
...event.parameters,
};
_resetComposePreview('outline', event.chapterCount);
_publishComposeGenerating(true);
_startComposeCommon(
emit: emit,
userId: event.userId,
novelId: event.novelId,
modelConfigId: event.modelConfigId,
prompt: event.prompt,
instructions: event.instructions,
settingSessionId: event.settingSessionId,
rawContextSelections: event.contextSelections,
parameters: composeParams,
startOperationText: '正在生成大纲...',
isPublicModel: event.isPublicModel,
publicModelConfigId: event.publicModelConfigId,
);
}
void _onStartComposeChapters(
StartComposeChaptersEvent event,
Emitter<SettingGenerationState> emit,
) {
final composeParams = {
'mode': 'chapters',
'chapterCount': event.chapterCount,
...event.parameters,
};
_resetComposePreview('chapters', event.chapterCount);
_publishComposeGenerating(true);
_startComposeCommon(
emit: emit,
userId: event.userId,
novelId: event.novelId,
modelConfigId: event.modelConfigId,
prompt: event.prompt,
instructions: event.instructions,
settingSessionId: event.settingSessionId,
rawContextSelections: event.contextSelections,
parameters: composeParams,
startOperationText: '正在生成章节...',
isPublicModel: event.isPublicModel,
publicModelConfigId: event.publicModelConfigId,
);
}
void _onStartComposeBundle(
StartComposeBundleEvent event,
Emitter<SettingGenerationState> emit,
) {
final composeParams = {
'mode': 'outline_plus_chapters',
'chapterCount': event.chapterCount,
...event.parameters,
};
_resetComposePreview('outline_plus_chapters', event.chapterCount);
_publishComposeGenerating(true);
_startComposeCommon(
emit: emit,
userId: event.userId,
novelId: event.novelId,
modelConfigId: event.modelConfigId,
prompt: event.prompt,
instructions: event.instructions,
settingSessionId: event.settingSessionId,
rawContextSelections: event.contextSelections,
parameters: composeParams,
startOperationText: '正在生成大纲与章节...',
isPublicModel: event.isPublicModel,
publicModelConfigId: event.publicModelConfigId,
);
}
void _onRefineCompose(
RefineComposeEvent event,
Emitter<SettingGenerationState> emit,
) {
final composeParams = {
...event.parameters,
};
_startComposeCommon(
emit: emit,
userId: event.userId,
novelId: event.novelId,
modelConfigId: event.modelConfigId,
prompt: null,
instructions: event.instructions,
settingSessionId: event.settingSessionId,
rawContextSelections: event.contextSelections,
parameters: composeParams,
startOperationText: '正在根据指令微调...',
);
}
void _onCancelCompose(
CancelComposeEvent event,
Emitter<SettingGenerationState> emit,
) {
_composeStreamSubscription?.cancel();
if (state is SettingGenerationInProgress) {
final s = state as SettingGenerationInProgress;
emit(s.copyWith(
isGenerating: false,
currentOperation: '已取消写作编排',
));
}
}
// 新增:在本地会话列表中把 novelId 绑定到指定 sessionId
void _updateSessionNovelId(String sessionId, String novelId) {
try {
if (novelId.isEmpty || sessionId.isEmpty) return;
if (state is SettingGenerationInProgress) {
final currentState = state as SettingGenerationInProgress;
// 仅当目标session是当前活跃会话时更新
if (currentState.activeSessionId == sessionId) {
final updatedActive = currentState.activeSession.copyWith(novelId: novelId);
final updatedSessions = currentState.sessions.map((s) => s.sessionId == sessionId ? updatedActive : s).toList();
emit(currentState.copyWith(activeSession: updatedActive, sessions: updatedSessions));
}
return;
}
if (state is SettingGenerationCompleted) {
final currentState = state as SettingGenerationCompleted;
if (currentState.activeSessionId == sessionId) {
final updatedActive = currentState.activeSession.copyWith(novelId: novelId);
final updatedSessions = currentState.sessions.map((s) => s.sessionId == sessionId ? updatedActive : s).toList();
emit(currentState.copyWith(activeSession: updatedActive, sessions: updatedSessions));
}
return;
}
} catch (_) {}
}
void _startComposeCommon({
required Emitter<SettingGenerationState> emit,
required String userId,
String? novelId,
required String modelConfigId,
String? prompt,
String? instructions,
String? settingSessionId,
Map<String, dynamic>? rawContextSelections,
required Map<String, dynamic> parameters,
required String startOperationText,
bool? isPublicModel,
String? publicModelConfigId,
}) {
// 不触发设定树状态切换,避免 UI 刷新
_markActivityAndResetTimeout();
// 若未传入,尽力从当前状态补齐 novelId / settingSessionId
String? effectiveNovelId = novelId;
String? effectiveSessionId = settingSessionId;
if (effectiveSessionId == null) {
if (state is SettingGenerationInProgress) {
effectiveSessionId = (state as SettingGenerationInProgress).activeSessionId;
effectiveNovelId ??= (state as SettingGenerationInProgress).activeSession.novelId;
} else if (state is SettingGenerationReady) {
final s = state as SettingGenerationReady;
effectiveSessionId = s.activeSessionId;
} else if (state is SettingGenerationCompleted) {
final s = state as SettingGenerationCompleted;
effectiveSessionId = s.activeSessionId;
effectiveNovelId ??= s.activeSession.novelId;
}
}
// 组装通用请求在BLoC层完成参数拼接
final requestJson = <String, dynamic>{
'requestType': AIRequestType.novelCompose.value,
'userId': userId,
if (effectiveNovelId != null) 'novelId': effectiveNovelId,
if (effectiveSessionId != null) 'settingSessionId': effectiveSessionId,
if (prompt != null) 'prompt': prompt,
if (instructions != null) 'instructions': instructions,
'parameters': parameters,
'metadata': {
'modelConfigId': modelConfigId,
if (isPublicModel == true) 'isPublicModel': true,
if (publicModelConfigId != null) 'publicModelConfigId': publicModelConfigId,
},
if (rawContextSelections != null) 'contextSelections': rawContextSelections['contextSelections'],
if (rawContextSelections != null && rawContextSelections['enableSmartContext'] != null)
'enableSmartContext': rawContextSelections['enableSmartContext'],
};
// 调试:关键元数据
try {
AppLogger.d(_tag, '[Compose] building request: modelConfigId=$modelConfigId, novelId=' +
(effectiveNovelId ?? 'null') + ', settingSessionId=' + (effectiveSessionId ?? 'null'));
} catch (_) {}
final request = UniversalAIRequest.fromJson(requestJson);
_composeStreamSubscription?.cancel();
_composeStreamSubscription = _repository.composeStream(request: request).listen(
(resp) {
// 不更新设定树状态,由结果预览模块单独消费内容
_markActivityAndResetTimeout();
// 将分片推入UI预览解析
try {
_handleComposeChunk(resp);
} catch (_) {}
},
onError: (error, stackTrace) {
_timeoutTimer?.cancel();
_timeoutTimer = null;
add(_HandleGenerationErrorInternal(error, stackTrace));
},
onDone: () {
_timeoutTimer?.cancel();
_timeoutTimer = null;
add(const _HandleGenerationCompleteInternal());
_publishComposeGenerating(false);
},
);
}
void _markActivityAndResetTimeout() {
_lastActivityAt = DateTime.now();
_resetInactivityTimeout();
}
/// 加载可用策略
///
/// 支持同时加载历史记录(如果提供了相关参数)
Future<void> _onLoadStrategies(
LoadStrategiesEvent event,
Emitter<SettingGenerationState> emit,
) async {
// 检查是否已经加载了策略,避免重复加载
if (state is SettingGenerationReady ||
state is SettingGenerationInProgress ||
state is SettingGenerationCompleted) {
AppLogger.i(_tag, '策略已加载,跳过重复加载');
return;
}
// 未登录时不发起网络请求
final String? uid = AppConfig.userId;
if (uid == null || uid.isEmpty) {
AppLogger.i(_tag, '未登录,跳过加载策略与历史记录');
return;
}
try {
emit(const SettingGenerationLoading(message: '正在加载生成策略...'));
final strategies = await _repository.getAvailableStrategies();
// 游客模式下不拉取历史记录;仅已登录且有 userId 时加载
List<Map<String, dynamic>> histories = [];
final String? currentUserId = AppConfig.userId;
if (currentUserId != null && currentUserId.isNotEmpty) {
try {
AppLogger.i(_tag, '加载当前用户历史记录, novelId=${event.novelId}');
histories = await _repository.getUserHistories(novelId: event.novelId);
AppLogger.i(_tag, '成功加载${histories.length}条历史记录');
} catch (e) {
AppLogger.error(_tag, '加载历史记录失败,但继续执行', e);
// 历史记录加载失败不影响策略加载
}
} else {
AppLogger.i(_tag, '未登录,跳过加载历史记录');
}
// 转换历史记录为Session对象为了兼容现有逻辑
final sessions = histories.map((history) {
return SettingGenerationSession.fromJson(history);
}).toList();
emit(SettingGenerationReady(
strategies: strategies,
sessions: sessions,
));
// 若已登录但 sessions 为空,尝试主动加载一次历史记录列表
final uid = AppConfig.userId;
if ((uid != null && uid.isNotEmpty) && sessions.isEmpty) {
add(const GetUserHistoriesEvent());
}
} catch (e, stackTrace) {
AppLogger.error(_tag, '加载策略失败', e, stackTrace);
emit(SettingGenerationError(
message: '加载生成策略失败:${e.toString()}',
error: e,
stackTrace: stackTrace,
));
}
}
/// 加载历史记录
///
/// 使用用户维度的历史记录管理
Future<void> _onLoadHistories(
LoadHistoriesEvent event,
Emitter<SettingGenerationState> emit,
) async {
if (state is! SettingGenerationReady) {
AppLogger.w(_tag, '当前状态不支持加载历史记录: ${state.runtimeType}');
return;
}
try {
AppLogger.i(_tag, '加载历史记录: novelId=${event.novelId}, userId=${event.userId}');
final currentState = state as SettingGenerationReady;
emit(const SettingGenerationLoading(message: '正在加载历史记录...'));
// 使用新的用户维度历史记录API
final histories = await _repository.getUserHistories(
novelId: event.novelId,
page: event.page,
size: event.size,
);
// 转换为Session对象
final sessions = histories.map((history) {
return SettingGenerationSession.fromJson(history);
}).toList();
emit(currentState.copyWith(
sessions: sessions,
));
AppLogger.i(_tag, '成功加载${sessions.length}条历史记录');
} catch (e, stackTrace) {
AppLogger.error(_tag, '加载历史记录失败', e, stackTrace);
emit(SettingGenerationError(
message: '加载历史记录失败:${e.toString()}',
error: e,
stackTrace: stackTrace,
));
}
}
/// 从小说设定创建编辑会话
///
/// 支持用户选择创建新快照或编辑上次设定
Future<void> _onStartSessionFromNovel(
StartSessionFromNovelEvent event,
Emitter<SettingGenerationState> emit,
) async {
try {
AppLogger.i(_tag, '从小说设定创建编辑会话: ${event.novelId}, createNewSnapshot: ${event.createNewSnapshot}');
emit(const SettingGenerationLoading(message: '正在创建编辑会话...'));
final result = await _repository.startSessionFromNovel(
novelId: event.novelId,
editReason: event.editReason,
modelConfigId: event.modelConfigId,
createNewSnapshot: event.createNewSnapshot,
);
// 解析返回结果
final sessionId = result['sessionId'] as String;
final hasExistingHistory = result['hasExistingHistory'] as bool? ?? false;
final snapshotMode = result['snapshotMode'] as String? ?? 'new';
// 获取当前策略和会话列表
final currentState = state;
List<StrategyTemplateInfo> strategies = [];
List<SettingGenerationSession> sessions = [];
if (currentState is SettingGenerationReady) {
strategies = currentState.strategies;
sessions = currentState.sessions;
}
// 创建会话对象
final session = SettingGenerationSession(
sessionId: sessionId,
userId: AppConfig.userId ?? 'current_user',
novelId: event.novelId,
initialPrompt: event.editReason,
strategy: '编辑模式',
modelConfigId: event.modelConfigId,
status: SessionStatus.completed, // 编辑会话直接为完成状态
createdAt: DateTime.now(),
rootNodes: [], // 节点数据将从后端获取
);
final updatedSessions = [session, ...sessions];
emit(SettingGenerationCompleted(
strategies: strategies,
sessions: updatedSessions,
activeSessionId: sessionId,
activeSession: session,
message: hasExistingHistory ? '已加载上次设定进行编辑' : '已创建新的设定快照',
// 🔧 关键修复:确保所有节点都可见
renderedNodeIds: _collectAllNodeIds(session.rootNodes).toSet(),
));
AppLogger.i(_tag, '编辑会话创建成功: $sessionId, 快照模式: $snapshotMode');
} catch (e, stackTrace) {
AppLogger.error(_tag, '创建编辑会话失败', e, stackTrace);
emit(SettingGenerationError(
message: '创建编辑会话失败:${e.toString()}',
error: e,
stackTrace: stackTrace,
));
}
}
/// 开始生成
Future<void> _onStartGeneration(
StartGenerationEvent event,
Emitter<SettingGenerationState> emit,
) async {
try {
// 🔧 新增检查和设置测试用户ID仅用于开发环境
if (AppConfig.userId == null || AppConfig.userId!.isEmpty) {
const testUserId = 'test_user_67d67d6833335f5166782e6f'; // 使用固定的测试用户ID
AppConfig.setUserId(testUserId);
AppLogger.w(_tag, '⚠️ 设置测试用户ID: $testUserId(仅用于开发环境)');
}
// 🔧 修复:允许从错误状态重试
if (state is! SettingGenerationReady &&
state is! SettingGenerationCompleted &&
state is! SettingGenerationInProgress &&
state is! SettingGenerationError) {
emit(const SettingGenerationError(
message: '系统未初始化完成,请稍后再试',
isRecoverable: true,
));
return;
}
// 🔧 新增:如果当前是错误状态,先重置为准备状态
if (state is SettingGenerationError) {
AppLogger.w(_tag, '🔄 从错误状态重试生成,先重置状态');
emit(const SettingGenerationLoading(message: '正在重置状态...'));
// 获取策略数据(如果有的话)
try {
final strategies = await _repository.getAvailableStrategies();
emit(SettingGenerationReady(
strategies: strategies,
sessions: [],
));
} catch (e) {
AppLogger.error(_tag, '重置状态失败', e);
emit(SettingGenerationError(
message: '重置失败,请刷新页面重试:${e.toString()}',
error: e,
isRecoverable: false,
));
return;
}
}
final currentState = state;
List<StrategyTemplateInfo> strategies = [];
List<SettingGenerationSession> sessions = [];
if (currentState is SettingGenerationReady) {
strategies = currentState.strategies;
sessions = currentState.sessions;
} else if (currentState is SettingGenerationInProgress) {
strategies = currentState.strategies;
sessions = currentState.sessions;
} else if (currentState is SettingGenerationCompleted) {
strategies = currentState.strategies;
sessions = currentState.sessions;
} else if (currentState is SettingGenerationError) {
// 从错误状态恢复,仅保留会话列表
sessions = currentState.sessions;
}
// 创建新会话
final sessionId = 'session_${DateTime.now().millisecondsSinceEpoch}';
final newSession = SettingGenerationSession(
sessionId: sessionId,
userId: event.userId ?? AppConfig.userId ?? 'default_user',
novelId: event.novelId,
initialPrompt: event.initialPrompt,
strategy: event.promptTemplateId,
modelConfigId: event.modelConfigId,
status: SessionStatus.initializing,
createdAt: DateTime.now(),
);
final updatedSessions = [newSession, ...sessions];
emit(SettingGenerationInProgress(
strategies: strategies,
sessions: updatedSessions,
activeSessionId: sessionId,
activeSession: newSession,
isGenerating: true,
currentOperation: '正在初始化生成会话...',
));
// 启动真实的生成流
AppLogger.i(_tag, '🚀 启动生成流程');
// 启动/重置基于最后活动时间的超时定时器
_markActivityAndResetTimeout();
// 监听生成流
_generationStreamSubscription?.cancel();
_generationStreamSubscription = _repository.startGeneration(
initialPrompt: event.initialPrompt,
promptTemplateId: event.promptTemplateId,
novelId: event.novelId,
modelConfigId: event.modelConfigId,
userId: event.userId,
usePublicTextModel: event.usePublicTextModel,
textPhasePublicProvider: event.textPhasePublicProvider,
textPhasePublicModelId: event.textPhasePublicModelId,
).listen(
(generationEvent) {
add(_HandleGenerationEventInternal(generationEvent));
},
onError: (error, stackTrace) {
AppLogger.error(_tag, '生成流错误', error, stackTrace);
// 取消超时定时器
_timeoutTimer?.cancel();
_timeoutTimer = null;
String userFriendlyMessage = _getUserFriendlyErrorMessage(error);
add(_HandleGenerationErrorInternal(error, stackTrace, userFriendlyMessage));
},
onDone: () {
AppLogger.info(_tag, '生成流完成');
// 取消超时定时器
_timeoutTimer?.cancel();
_timeoutTimer = null;
add(const _HandleGenerationCompleteInternal());
},
);
} catch (e, stackTrace) {
AppLogger.error(_tag, '开始生成失败', e, stackTrace);
String userFriendlyMessage = _getUserFriendlyErrorMessage(e);
emit(SettingGenerationError(
message: userFriendlyMessage,
error: e,
stackTrace: stackTrace,
isRecoverable: _isRecoverableError(e),
));
}
}
/// 基于当前会话的整体调整生成
Future<void> _onAdjustGeneration(
AdjustGenerationEvent event,
Emitter<SettingGenerationState> emit,
) async {
try {
// 仅允许在有会话的状态下调整
if (state is! SettingGenerationInProgress && state is! SettingGenerationCompleted) {
emit(const SettingGenerationError(
message: '当前没有可调整的会话,请先生成或加载历史记录',
));
return;
}
// 取现有策略/会话用于维持UI
List<StrategyTemplateInfo> strategies = [];
List<SettingGenerationSession> sessions = [];
String activeSessionId = '';
SettingGenerationSession? activeSession;
if (state is SettingGenerationInProgress) {
final s = state as SettingGenerationInProgress;
strategies = s.strategies;
sessions = s.sessions;
activeSessionId = s.activeSessionId;
activeSession = s.activeSession;
} else if (state is SettingGenerationCompleted) {
final s = state as SettingGenerationCompleted;
strategies = s.strategies;
sessions = s.sessions;
activeSessionId = s.activeSessionId;
activeSession = s.activeSession;
}
// 校验 session 一致
if (activeSessionId.isEmpty || activeSession == null || activeSessionId != event.sessionId) {
AppLogger.w(_tag, 'AdjustGenerationEvent 的 sessionId 与当前会话不一致使用事件给定的sessionId继续');
activeSessionId = event.sessionId;
}
// 进入进行中状态,展示生成中提示
emit(SettingGenerationInProgress(
strategies: strategies,
sessions: sessions,
activeSessionId: activeSessionId,
activeSession: activeSession ?? sessions.firstWhere((s) => s.sessionId == activeSessionId, orElse: () => sessions.first),
isGenerating: true,
currentOperation: '正在基于当前会话整体调整...',
adjustmentPrompt: event.adjustmentPrompt,
));
// 启动/重置超时
_markActivityAndResetTimeout();
// 打开 SSE 流
_generationStreamSubscription?.cancel();
_generationStreamSubscription = _repository.adjustSession(
sessionId: activeSessionId,
adjustmentPrompt: event.adjustmentPrompt,
modelConfigId: event.modelConfigId,
promptTemplateId: event.promptTemplateId ?? activeSession?.metadata['promptTemplateId'],
).listen(
(generationEvent) {
add(_HandleGenerationEventInternal(generationEvent));
},
onError: (error, stackTrace) {
AppLogger.error(_tag, '调整生成流错误', error, stackTrace);
_timeoutTimer?.cancel();
_timeoutTimer = null;
add(_HandleGenerationErrorInternal(error, stackTrace));
},
onDone: () {
AppLogger.info(_tag, '调整生成流完成');
_timeoutTimer?.cancel();
_timeoutTimer = null;
add(const _HandleGenerationCompleteInternal());
},
);
} catch (e, stackTrace) {
AppLogger.error(_tag, '调整生成失败', e, stackTrace);
emit(SettingGenerationError(
message: '调整生成失败:${e.toString()}',
error: e,
stackTrace: stackTrace,
isRecoverable: true,
sessions: _getCurrentSessions(),
));
}
}
/// 更新节点
Future<void> _onUpdateNode(
UpdateNodeEvent event,
Emitter<SettingGenerationState> emit,
) async {
// 🔧 修复:支持从多种状态开始节点修改
String? sessionId;
SettingGenerationSession? activeSession;
List<StrategyTemplateInfo> strategies = [];
List<SettingGenerationSession> sessions = [];
String? selectedNodeId;
String viewMode = 'compact';
String adjustmentPrompt = '';
Map<String, SettingNode> pendingChanges = {};
Set<String> highlightedNodeIds = {};
Map<String, List<SettingNode>> editHistory = {};
List<event_model.SettingGenerationEvent> events = [];
Map<String, NodeRenderInfo> nodeRenderStates = {};
Set<String> renderedNodeIds = {};
if (state is SettingGenerationInProgress) {
final currentState = state as SettingGenerationInProgress;
sessionId = currentState.activeSessionId;
activeSession = currentState.activeSession;
strategies = currentState.strategies;
sessions = currentState.sessions;
selectedNodeId = currentState.selectedNodeId;
viewMode = currentState.viewMode;
adjustmentPrompt = currentState.adjustmentPrompt;
pendingChanges = currentState.pendingChanges;
highlightedNodeIds = currentState.highlightedNodeIds;
editHistory = currentState.editHistory;
events = currentState.events;
nodeRenderStates = currentState.nodeRenderStates;
renderedNodeIds = currentState.renderedNodeIds;
} else if (state is SettingGenerationCompleted) {
final currentState = state as SettingGenerationCompleted;
// 🔧 关键修复使用historyId作为sessionId
sessionId = currentState.activeSession.historyId ?? currentState.activeSession.sessionId;
activeSession = currentState.activeSession;
strategies = currentState.strategies;
sessions = currentState.sessions;
selectedNodeId = currentState.selectedNodeId;
viewMode = currentState.viewMode;
adjustmentPrompt = currentState.adjustmentPrompt;
pendingChanges = currentState.pendingChanges;
highlightedNodeIds = currentState.highlightedNodeIds;
editHistory = currentState.editHistory;
events = currentState.events;
nodeRenderStates = currentState.nodeRenderStates;
renderedNodeIds = currentState.renderedNodeIds;
} else if (state is SettingGenerationNodeUpdating) {
final currentState = state as SettingGenerationNodeUpdating;
sessionId = currentState.activeSessionId;
activeSession = currentState.activeSession;
strategies = currentState.strategies;
sessions = currentState.sessions;
selectedNodeId = currentState.selectedNodeId;
viewMode = currentState.viewMode;
adjustmentPrompt = currentState.adjustmentPrompt;
pendingChanges = currentState.pendingChanges;
highlightedNodeIds = currentState.highlightedNodeIds;
editHistory = currentState.editHistory;
events = currentState.events;
nodeRenderStates = currentState.nodeRenderStates;
renderedNodeIds = currentState.renderedNodeIds;
} else {
emit(const SettingGenerationError(message: '当前状态不支持节点修改'));
return;
}
try {
AppLogger.i(_tag, '🔧 开始节点修改 - sessionId: $sessionId, nodeId: ${event.nodeId}');
// 🔧 修复使用新的SettingGenerationNodeUpdating状态避免整个设定树重新渲染
emit(SettingGenerationNodeUpdating(
strategies: strategies,
sessions: sessions,
activeSessionId: sessionId,
activeSession: activeSession,
selectedNodeId: selectedNodeId,
viewMode: viewMode,
adjustmentPrompt: adjustmentPrompt,
pendingChanges: pendingChanges,
highlightedNodeIds: highlightedNodeIds,
editHistory: editHistory,
events: events,
updatingNodeId: event.nodeId,
modificationPrompt: event.modificationPrompt,
scope: event.scope,
isUpdating: true,
message: '正在根据提示修改节点内容,请稍候...',
nodeRenderStates: nodeRenderStates,
renderedNodeIds: renderedNodeIds,
));
// 启动/重置基于最后活动时间的超时定时器
_markActivityAndResetTimeout();
_updateStreamSubscription?.cancel();
_updateStreamSubscription = _repository.updateNode(
sessionId: sessionId,
nodeId: event.nodeId,
modificationPrompt: event.modificationPrompt,
modelConfigId: event.modelConfigId,
scope: event.scope,
).listen(
(generationEvent) {
AppLogger.i(_tag, '📡 收到节点修改事件: ${generationEvent.eventType}');
add(_HandleGenerationEventInternal(generationEvent));
},
onError: (error, stackTrace) {
AppLogger.error(_tag, '更新节点流错误', error, stackTrace);
// 取消超时定时器
_timeoutTimer?.cancel();
_timeoutTimer = null;
// NodeUpdating阶段不进入错误态直接结束保持原态Toast 交给外层 Screen 监听错误状态触发)
add(_HandleGenerationCompleteInternal());
},
onDone: () {
AppLogger.info(_tag, '更新节点流完成');
// 取消超时定时器
_timeoutTimer?.cancel();
_timeoutTimer = null;
add(const _HandleGenerationCompleteInternal());
},
);
} catch (e, stackTrace) {
AppLogger.error(_tag, '更新节点失败', e, stackTrace);
emit(SettingGenerationError(
message: '更新节点失败:${e.toString()}',
error: e,
stackTrace: stackTrace,
sessions: sessions,
activeSessionId: sessionId,
));
}
}
/// 更新节点内容
/// 直接调用后端API更新节点内容
void _onUpdateNodeContent(
UpdateNodeContentEvent event,
Emitter<SettingGenerationState> emit,
) async {
try {
// 获取当前会话ID
String? sessionId;
if (state is SettingGenerationInProgress) {
sessionId = (state as SettingGenerationInProgress).activeSessionId;
} else if (state is SettingGenerationCompleted) {
sessionId = (state as SettingGenerationCompleted).activeSession.sessionId;
} else {
// 修改:当没有活跃会话时,静默忽略而不是报错
AppLogger.info(_tag, '没有活跃会话,忽略节点内容更新: ${event.nodeId}');
return;
}
// 🔧 新增:调试日志
final currentUserId = AppConfig.userId;
AppLogger.i(_tag, '🔧 准备更新节点内容: sessionId=$sessionId, nodeId=${event.nodeId}, userId=$currentUserId');
// 先在本地更新UI状态
if (state is SettingGenerationInProgress) {
final currentState = state as SettingGenerationInProgress;
final updatedNodes = _updateNodeContentInTree(
currentState.activeSession.rootNodes,
event.nodeId,
event.content,
);
final updatedSession = currentState.activeSession.copyWith(
rootNodes: updatedNodes,
);
emit(currentState.copyWith(
activeSession: updatedSession,
));
} else if (state is SettingGenerationCompleted) {
final currentState = state as SettingGenerationCompleted;
final updatedNodes = _updateNodeContentInTree(
currentState.activeSession.rootNodes,
event.nodeId,
event.content,
);
final updatedSession = currentState.activeSession.copyWith(
rootNodes: updatedNodes,
);
emit(currentState.copyWith(
activeSession: updatedSession,
));
}
// 异步调用后端API保存更改
// 🔧 新增API调用前日志
AppLogger.i(_tag, '🚀 开始调用后端API更新节点内容: sessionId=$sessionId, nodeId=${event.nodeId}');
try {
await _repository.updateNodeContent(
sessionId: sessionId,
nodeId: event.nodeId,
newContent: event.content,
);
// 🔧 新增API调用成功日志
AppLogger.i(_tag, '✅ 后端API调用成功: sessionId=$sessionId, nodeId=${event.nodeId}');
} catch (e, stackTrace) {
// 🔧 增强:错误日志
AppLogger.error(_tag, '❌ 后端API调用失败: sessionId=$sessionId, nodeId=${event.nodeId}, error=${e.toString()}', e, stackTrace);
// 可选发送错误状态给UI
emit(SettingGenerationError(
message: '保存节点内容失败:${e.toString()}',
error: e,
stackTrace: stackTrace,
));
}
} catch (e, stackTrace) {
AppLogger.error(_tag, '更新节点内容失败', e, stackTrace);
// 修改:不再因为更新节点内容失败而发出错误状态,避免影响用户体验
AppLogger.info(_tag, '节点内容更新失败但不影响UI状态');
}
}
Timer? _pendingNodesTimer;
void _debounceProcessPendingNodes() {
_pendingNodesTimer?.cancel();
// 🔧 减少延迟时间,更快处理节点
_pendingNodesTimer = Timer(const Duration(milliseconds: 50), () {
if (!isClosed) {
add(const _ProcessPendingNodes());
}
});
}
/// 🚀 新增:智能拓扑排序,立即处理可渲染的节点
void _processNodesImmediately(
List<event_model.NodeCreatedEvent> newNodes,
Emitter<SettingGenerationState> emit,
) {
if (state is! SettingGenerationInProgress) return;
final currentState = state as SettingGenerationInProgress;
final existingNodes = currentState.activeSession.rootNodes;
// 找出可以立即渲染的节点(没有父节点或父节点已存在)
final immediatelyRenderableNodes = <event_model.NodeCreatedEvent>[];
final needsWaitingNodes = <event_model.NodeCreatedEvent>[];
for (final nodeEvent in newNodes) {
final node = nodeEvent.node;
final parentId = node.parentId;
if (parentId == null) {
// 根节点,可以立即渲染
AppLogger.i(_tag, '⚡ 立即处理根节点: ${node.name}');
immediatelyRenderableNodes.add(nodeEvent);
} else {
// 检查父节点是否已存在
final parentExists = SettingNodeUtils.findNodeInTree(existingNodes, parentId) != null;
if (parentExists) {
AppLogger.i(_tag, '⚡ 父节点已存在,立即处理: ${node.name}');
immediatelyRenderableNodes.add(nodeEvent);
} else {
AppLogger.i(_tag, '⏳ 父节点不存在,暂存等待: ${node.name}');
needsWaitingNodes.add(nodeEvent);
}
}
}
// 立即处理可渲染的节点
if (immediatelyRenderableNodes.isNotEmpty) {
_insertNodesAndTriggerRender(immediatelyRenderableNodes, emit);
}
// 将需要等待的节点加入暂存队列
if (needsWaitingNodes.isNotEmpty) {
final updatedPendingNodes = List<event_model.NodeCreatedEvent>.from(currentState.pendingNodes)
..addAll(needsWaitingNodes);
emit(currentState.copyWith(pendingNodes: updatedPendingNodes));
// 对暂存节点使用短延迟处理
_debounceProcessPendingNodes();
}
}
/// 插入节点并触发渲染
void _insertNodesAndTriggerRender(
List<event_model.NodeCreatedEvent> nodeEvents,
Emitter<SettingGenerationState> emit,
) {
if (state is! SettingGenerationInProgress) return;
final currentState = state as SettingGenerationInProgress;
var currentNodes = currentState.activeSession.rootNodes;
var updatedRenderQueue = List<String>.from(currentState.renderQueue);
var updatedNodeRenderStates = Map<String, NodeRenderInfo>.from(currentState.nodeRenderStates);
// 使用改进的拓扑排序
final sortedEvents = _improvedTopologicalSort(nodeEvents, currentNodes);
AppLogger.i(_tag, '🎯 立即插入 ${sortedEvents.length} 个节点');
// 批量插入节点
for (final nodeEvent in sortedEvents) {
currentNodes = _insertNodeIntoTree(
currentNodes,
nodeEvent.node,
nodeEvent.parentPath,
);
updatedRenderQueue.add(nodeEvent.node.id);
updatedNodeRenderStates[nodeEvent.node.id] = NodeRenderInfo(
nodeId: nodeEvent.node.id,
state: NodeRenderState.pending,
);
}
final updatedSession = currentState.activeSession.copyWith(rootNodes: currentNodes);
final updatedSessions = currentState.sessions.map((session) {
return session.sessionId == currentState.activeSessionId ? updatedSession : session;
}).toList();
emit(currentState.copyWith(
sessions: updatedSessions,
activeSession: updatedSession,
renderQueue: updatedRenderQueue,
nodeRenderStates: updatedNodeRenderStates,
// 统一文案,避免与后续显示重复
currentOperation: '已处理 ${sortedEvents.length} 个新节点',
));
// 立即触发渲染队列处理
add(const ProcessRenderQueueEvent());
}
/// 在设定节点树中更新指定节点的内容
List<SettingNode> _updateNodeContentInTree(
List<SettingNode> nodes,
String nodeId,
String newContent,
) {
return nodes.map((node) {
if (node.id == nodeId) {
return node.copyWith(description: newContent);
} else if (node.children != null && node.children!.isNotEmpty) {
return node.copyWith(
children: _updateNodeContentInTree(node.children!, nodeId, newContent),
);
} else {
return node;
}
}).toList();
}
/// 选择节点
void _onSelectNode(
SelectNodeEvent event,
Emitter<SettingGenerationState> emit,
) {
if (state is SettingGenerationInProgress) {
final currentState = state as SettingGenerationInProgress;
emit(currentState.copyWith(selectedNodeId: event.nodeId));
} else if (state is SettingGenerationCompleted) {
final currentState = state as SettingGenerationCompleted;
emit(currentState.copyWith(selectedNodeId: event.nodeId));
}
}
/// 切换视图模式
void _onToggleViewMode(
ToggleViewModeEvent event,
Emitter<SettingGenerationState> emit,
) {
if (state is SettingGenerationReady) {
final currentState = state as SettingGenerationReady;
emit(currentState.copyWith(viewMode: event.viewMode));
} else if (state is SettingGenerationInProgress) {
final currentState = state as SettingGenerationInProgress;
emit(currentState.copyWith(viewMode: event.viewMode));
} else if (state is SettingGenerationCompleted) {
final currentState = state as SettingGenerationCompleted;
emit(currentState.copyWith(viewMode: event.viewMode));
}
}
/// 应用待处理的更改
void _onApplyPendingChanges(
ApplyPendingChangesEvent event,
Emitter<SettingGenerationState> emit,
) {
if (state is! SettingGenerationInProgress) return;
final currentState = state as SettingGenerationInProgress;
if (currentState.pendingChanges.isEmpty) return;
// 更新会话中的节点数据
final updatedNodes = _applyChangesToNodes(
currentState.activeSession.rootNodes,
currentState.pendingChanges,
);
// 更新编辑历史
final newHistory = Map<String, List<SettingNode>>.from(currentState.editHistory);
for (final entry in currentState.pendingChanges.entries) {
final nodeId = entry.key;
final originalNode = SettingNodeUtils.findNodeInTree(currentState.activeSession.rootNodes, nodeId);
if (originalNode != null) {
newHistory[nodeId] = [...(newHistory[nodeId] ?? []), originalNode];
}
}
final updatedSession = currentState.activeSession.copyWith(
rootNodes: updatedNodes,
);
final updatedSessions = currentState.sessions.map((session) {
return session.sessionId == currentState.activeSessionId ? updatedSession : session;
}).toList();
emit(currentState.copyWith(
sessions: updatedSessions,
activeSession: updatedSession,
pendingChanges: {},
highlightedNodeIds: {},
editHistory: newHistory,
));
}
/// 取消待处理的更改
void _onCancelPendingChanges(
CancelPendingChangesEvent event,
Emitter<SettingGenerationState> emit,
) {
if (state is SettingGenerationInProgress) {
final currentState = state as SettingGenerationInProgress;
emit(currentState.copyWith(
pendingChanges: {},
highlightedNodeIds: {},
));
}
}
/// 撤销节点更改
void _onUndoNodeChange(
UndoNodeChangeEvent event,
Emitter<SettingGenerationState> emit,
) {
if (state is! SettingGenerationInProgress) return;
final currentState = state as SettingGenerationInProgress;
final nodeHistory = currentState.editHistory[event.nodeId];
if (nodeHistory == null || nodeHistory.isEmpty) return;
final previousState = nodeHistory.last;
final updatedNodes = _updateNodeInTree(
currentState.activeSession.rootNodes,
event.nodeId,
previousState,
);
final newHistory = Map<String, List<SettingNode>>.from(currentState.editHistory);
newHistory[event.nodeId] = nodeHistory.sublist(0, nodeHistory.length - 1);
final updatedSession = currentState.activeSession.copyWith(
rootNodes: updatedNodes,
);
final updatedSessions = currentState.sessions.map((session) {
return session.sessionId == currentState.activeSessionId ? updatedSession : session;
}).toList();
emit(currentState.copyWith(
sessions: updatedSessions,
activeSession: updatedSession,
editHistory: newHistory,
));
}
/// 保存生成的设定
Future<void> _onSaveGeneratedSettings(
SaveGeneratedSettingsEvent event,
Emitter<SettingGenerationState> emit,
) async {
if (state is! SettingGenerationInProgress && state is! SettingGenerationCompleted) {
emit(const SettingGenerationError(message: '没有可保存的设定'));
return;
}
try {
// 取消生成流,防止 SSE 连接在错误时无限重试
_generationStreamSubscription?.cancel();
String sessionId;
if (state is SettingGenerationInProgress) {
sessionId = (state as SettingGenerationInProgress).activeSessionId;
} else {
sessionId = (state as SettingGenerationCompleted).activeSessionId;
}
// 调用新的统一保存方法返回SaveResult
final saveResult = await _repository.saveGeneratedSettings(
sessionId: sessionId,
novelId: event.novelId,
updateExisting: event.updateExisting,
targetHistoryId: event.targetHistoryId,
);
// 从SaveResult中获取historyId
final String? historyId = saveResult.historyId;
final String successMessage = _getSuccessMessage(event.novelId, event.updateExisting);
// 更新会话状态
_updateSessionAfterSave(emit, historyId, successMessage);
} catch (e, stackTrace) {
AppLogger.error(_tag, '保存设定失败', e, stackTrace);
emit(SettingGenerationError(
message: '保存设定失败:${e.toString()}',
error: e,
stackTrace: stackTrace,
isRecoverable: true,
));
}
}
/// 处理生成超时
Future<void> _handleGenerationTimeout(Emitter<SettingGenerationState> emit) async {
try {
// 取消 SSE 连接
_generationStreamSubscription?.cancel();
_generationStreamSubscription = null;
// 如果有活跃会话,尝试取消后端任务
if (state is SettingGenerationInProgress) {
final currentState = state as SettingGenerationInProgress;
try {
await _repository.cancelSession(sessionId: currentState.activeSessionId);
AppLogger.i(_tag, '✅ 成功取消后端生成任务: ${currentState.activeSessionId}');
} catch (e) {
AppLogger.w(_tag, '⚠️ 取消后端任务失败,但继续处理超时: $e');
}
}
// 改为软提示:不切换到错误页,保持 InProgress 状态,提示并停止生成
if (state is SettingGenerationInProgress) {
final currentState = state as SettingGenerationInProgress;
emit(currentState.copyWith(
isGenerating: false,
currentOperation: '生成任务超时5分钟已自动取消。',
));
} else {
// 其他状态下,尽量不打断,仅作为可恢复错误
emit(SettingGenerationError(
message: '生成任务超时5分钟已自动取消。请稍后重试。',
error: TimeoutException('生成任务超时', const Duration(minutes: 5)),
stackTrace: StackTrace.current,
isRecoverable: true,
sessions: _getCurrentSessions(),
));
}
} catch (e, stackTrace) {
AppLogger.error(_tag, '处理超时时发生错误', e, stackTrace);
emit(SettingGenerationError(
message: '生成任务超时并处理失败,请重试。',
error: e,
stackTrace: stackTrace,
isRecoverable: true,
sessions: _getCurrentSessions(),
));
}
}
/// 基于最后活动时间的超时检查(由定时器派发,不在回调中直接 emit
Future<void> _onTimeoutCheckInternal(
_TimeoutCheckInternal event,
Emitter<SettingGenerationState> emit,
) async {
if (!(state is SettingGenerationInProgress || state is SettingGenerationNodeUpdating)) {
_timeoutTimer?.cancel();
_timeoutTimer = null;
return;
}
final DateTime now = DateTime.now();
final DateTime last = _lastActivityAt ?? now;
final bool isTimedOut = now.difference(last) >= _timeoutDuration;
if (isTimedOut) {
AppLogger.w(_tag, '⏰ 生成业务超时(基于最后活动时间 ${_timeoutDuration.inSeconds}s');
_timeoutTimer?.cancel();
_timeoutTimer = null;
if (emit.isDone) {
AppLogger.w(_tag, 'emit已完成跳过超时处理');
return;
}
await _handleGenerationTimeout(emit);
} else {
// 未超时则继续观察
_resetInactivityTimeout();
}
}
/// 获取当前会话列表的辅助方法
List<SettingGenerationSession> _getCurrentSessions() {
if (state is SettingGenerationInProgress) {
return (state as SettingGenerationInProgress).sessions;
} else if (state is SettingGenerationCompleted) {
return (state as SettingGenerationCompleted).sessions;
} else if (state is SettingGenerationError) {
return (state as SettingGenerationError).sessions;
}
return [];
}
/// 获取保存成功消息
String _getSuccessMessage(String? novelId, bool updateExisting) {
if (novelId == null) {
return '设定已成功保存为独立快照';
} else if (updateExisting) {
return '历史记录已成功更新';
} else {
return '设定已成功保存到小说中';
}
}
/// 保存后更新会话状态
void _updateSessionAfterSave(
Emitter<SettingGenerationState> emit,
String? historyId,
String message,
) {
if (state is SettingGenerationInProgress) {
final s = state as SettingGenerationInProgress;
final updatedActive = s.activeSession.copyWith(
status: SessionStatus.saved,
sessionId: historyId ?? s.activeSession.sessionId,
historyId: historyId,
);
final updatedSessions = s.sessions.map((sess) {
return sess.sessionId == s.activeSessionId ? updatedActive : sess;
}).toList();
emit(SettingGenerationCompleted(
strategies: s.strategies,
sessions: updatedSessions,
activeSessionId: updatedActive.sessionId,
activeSession: updatedActive,
selectedNodeId: s.selectedNodeId,
viewMode: s.viewMode,
adjustmentPrompt: s.adjustmentPrompt,
pendingChanges: s.pendingChanges,
highlightedNodeIds: s.highlightedNodeIds,
editHistory: s.editHistory,
events: s.events,
message: message,
nodeRenderStates: s.nodeRenderStates,
renderedNodeIds: s.renderedNodeIds,
));
} else if (state is SettingGenerationCompleted) {
final s = state as SettingGenerationCompleted;
final updatedActive = s.activeSession.copyWith(
status: SessionStatus.saved,
sessionId: historyId ?? s.activeSession.sessionId,
historyId: historyId,
);
final updatedSessions = s.sessions.map((sess) {
return sess.sessionId == s.activeSessionId ? updatedActive : sess;
}).toList();
emit(s.copyWith(
sessions: updatedSessions,
activeSession: updatedActive,
activeSessionId: updatedActive.sessionId,
message: message,
));
}
}
/// 创建新会话(在 Ready / Error 状态下均可触发)
Future<void> _onCreateNewSession(
CreateNewSessionEvent event,
Emitter<SettingGenerationState> emit,
) async {
// 1. Ready 状态:直接创建占位会话并设为激活
if (state is SettingGenerationReady) {
final currentState = state as SettingGenerationReady;
final placeholderSession = SettingGenerationSession(
sessionId: 'new_${DateTime.now().millisecondsSinceEpoch}',
userId: AppConfig.userId ?? 'current_user',
novelId: null,
initialPrompt: '',
strategy: '九线法',
status: SessionStatus.initializing,
createdAt: DateTime.now(),
rootNodes: const [],
);
emit(currentState.copyWith(
sessions: [placeholderSession, ...currentState.sessions],
activeSessionId: placeholderSession.sessionId,
adjustmentPrompt: '',
));
return;
}
// 2. Error 状态:尝试快速恢复到 Ready 状态,保留历史记录
if (state is SettingGenerationError) {
final errorState = state as SettingGenerationError;
// 显示轻量级的加载提示,避免整页闪烁
emit(const SettingGenerationLoading(message: '正在重新初始化...'));
// 尝试重新获取策略;若失败则使用默认策略占位
List<StrategyTemplateInfo> strategies = [];
try {
strategies = await _repository.getAvailableStrategies();
if (strategies.isEmpty) {
throw Exception('策略列表为空');
}
} catch (e) {
// 策略加载失败时直接抛出异常
AppLogger.error(_tag, '加载策略失败', e);
throw Exception('无法加载策略模板');
}
// 切换到 Ready 状态,清空当前激活会话但保留历史列表
emit(SettingGenerationReady(
strategies: strategies,
sessions: errorState.sessions,
activeSessionId: null,
));
}
}
/// 选择会话
void _onSelectSession(
SelectSessionEvent event,
Emitter<SettingGenerationState> emit,
) {
AppLogger.i(_tag, '选择会话: ' + event.sessionId + ', isHistory: ' + event.isHistorySession.toString());
if (state is SettingGenerationReady) {
final currentState = state as SettingGenerationReady;
final sessions = currentState.sessions;
if (sessions.isEmpty) {
emit(currentState.copyWith(
activeSessionId: null,
viewMode: 'compact',
adjustmentPrompt: '',
));
return;
}
final session = sessions.firstWhere(
(s) => s.sessionId == event.sessionId,
orElse: () => sessions.first,
);
// 切换会话时清空 novelId
final cleared = session.copyWith(novelId: '');
emit(currentState.copyWith(
activeSessionId: cleared.sessionId,
viewMode: 'compact',
adjustmentPrompt: '',
));
// 如果选择的是历史会话,需要切换到对应的状态
if (event.isHistorySession && session.status == SessionStatus.saved) {
emit(SettingGenerationCompleted(
strategies: currentState.strategies,
sessions: currentState.sessions,
activeSessionId: cleared.sessionId,
activeSession: cleared,
message: '已切换到历史会话',
));
}
return;
}
if (state is SettingGenerationInProgress) {
final s = state as SettingGenerationInProgress;
final session = s.sessions.firstWhere((ss) => ss.sessionId == event.sessionId,
orElse: () => s.sessions.isNotEmpty ? s.sessions.first : s.activeSession);
final cleared = session.copyWith(novelId: '');
emit(s.copyWith(
activeSessionId: cleared.sessionId,
activeSession: cleared,
renderedNodeIds: _collectAllNodeIds(cleared.rootNodes).toSet(),
selectedNodeId: null,
viewMode: 'compact',
adjustmentPrompt: '',
));
// 如果被选中的会话已经生成完成或已保存,则直接切换到 Completed 状态,避免动画
if (session.status == SessionStatus.completed || session.status == SessionStatus.saved) {
emit(SettingGenerationCompleted(
strategies: s.strategies,
sessions: s.sessions,
activeSessionId: cleared.sessionId,
activeSession: cleared,
message: '已切换到完成会话',
));
}
return;
}
if (state is SettingGenerationCompleted) {
final s = state as SettingGenerationCompleted;
final session = s.sessions.firstWhere((ss) => ss.sessionId == event.sessionId,
orElse: () => s.sessions.isNotEmpty ? s.sessions.first : s.activeSession);
final cleared = session.copyWith(novelId: '');
emit(s.copyWith(
activeSessionId: cleared.sessionId,
activeSession: cleared,
selectedNodeId: null,
viewMode: 'compact',
adjustmentPrompt: '',
));
return;
}
if (state is SettingGenerationError) {
_handleSelectSessionFromError(event, emit);
}
}
/// 🔧 新增:处理从错误状态选择会话的逻辑
Future<void> _handleSelectSessionFromError(
SelectSessionEvent event,
Emitter<SettingGenerationState> emit,
) async {
try {
final currentState = state as SettingGenerationError;
AppLogger.i(_tag, '🔄 从错误状态选择会话,尝试恢复: ${event.sessionId}');
// 查找对应的会话
final session = currentState.sessions.firstWhere(
(s) => s.sessionId == event.sessionId,
orElse: () => throw Exception('会话未找到: ${event.sessionId}'),
);
// 先显示加载状态
emit(const SettingGenerationLoading(message: '正在加载历史记录...'));
// 🔧 关键修复尝试重新加载策略数据确保UI有完整的数据支持
List<StrategyTemplateInfo> strategies = [];
try {
strategies = await _repository.getAvailableStrategies();
AppLogger.i(_tag, '✅ 成功重新加载策略数据: ${strategies.length}个策略');
} catch (e) {
AppLogger.w(_tag, '重新加载策略失败', e);
strategies = [];
}
// 🔧 修复:确保所有必要的状态字段都被正确初始化
emit(SettingGenerationCompleted(
strategies: strategies, // 使用重新加载的策略数据而不是空数组
sessions: currentState.sessions,
activeSessionId: event.sessionId,
activeSession: session,
message: '已加载历史设定',
// 🔧 关键修复:历史记录已完成,所有节点应该显示
nodeRenderStates: const {},
renderedNodeIds: _collectAllNodeIds(session.rootNodes).toSet(),
selectedNodeId: null,
viewMode: 'compact',
adjustmentPrompt: '',
pendingChanges: const {},
highlightedNodeIds: const {},
editHistory: const {},
events: const [],
));
AppLogger.i(_tag, '✅ 成功从错误状态恢复并加载历史记录: ${session.sessionId}');
} catch (e, stackTrace) {
AppLogger.error(_tag, '❌ 从错误状态选择会话失败', e, stackTrace);
// 如果恢复失败,保持在错误状态,但更新错误信息
emit(SettingGenerationError(
message: '加载历史记录失败:${e.toString()}',
error: e,
stackTrace: stackTrace,
isRecoverable: true,
sessions: (state as SettingGenerationError).sessions,
activeSessionId: event.sessionId,
));
}
}
/// 加载历史设定详情
Future<void> _onLoadHistoryDetail(
CreateSessionFromHistoryEvent event,
Emitter<SettingGenerationState> emit,
) async {
// 👉 在加载新的历史记录之前,确保取消任何仍在进行的流式生成或节点更新连接,
// 以防止 EventSource 在后台继续自动重连,导致不断重试 /setting-generation/start
_generationStreamSubscription?.cancel();
_generationStreamSubscription = null;
_updateStreamSubscription?.cancel();
_updateStreamSubscription = null;
try {
AppLogger.i(_tag, '加载历史设定详情: historyId=${event.historyId}');
// 解析当前状态用于保留策略和会话列表
final currentState = state;
List<StrategyTemplateInfo> strategies = [];
List<SettingGenerationSession> sessions = [];
if (currentState is SettingGenerationReady) {
strategies = currentState.strategies;
sessions = currentState.sessions;
} else if (currentState is SettingGenerationInProgress) {
strategies = currentState.strategies;
sessions = currentState.sessions;
} else if (currentState is SettingGenerationCompleted) {
strategies = currentState.strategies;
sessions = currentState.sessions;
} else if (currentState is SettingGenerationError) {
sessions = currentState.sessions;
// 从错误状态加载历史时,重新加载策略数据
try {
strategies = await _repository.getAvailableStrategies();
AppLogger.i(_tag, '重新加载策略数据: ${strategies.length}个策略');
} catch (e) {
AppLogger.w(_tag, '加载策略失败', e);
strategies = [];
}
}
// 加载历史记录详情
final historyDetail = await _repository.loadHistoryDetail(historyId: event.historyId);
// 后端返回格式: { history: {...}, rootNodes: [...] }
final historyJson = historyDetail['history'] as Map<String, dynamic>;
final rootNodesJson = historyDetail['rootNodes'] as List;
// 组合成一个完整的session对象
historyJson['rootNodes'] = rootNodesJson;
historyJson['sessionId'] = event.historyId;
// 处理 modelConfigId 为 null 的情况
if (historyJson['modelConfigId'] == null) {
historyJson['modelConfigId'] = event.modelConfigId;
}
final session0 = SettingGenerationSession.fromJson(historyJson);
// 切换/加载历史后,前端会话不应继承任何 novelId
final session = session0.copyWith(novelId: '');
AppLogger.i(_tag, '会话对象创建完成 - 节点数: ${session.rootNodes.length}');
// 更新或添加到会话列表(保持原有位置,不将选中的历史记录移到第一位)
List<SettingGenerationSession> updatedSessions;
final existingIndex = sessions.indexWhere((s) => s.sessionId == session.sessionId);
if (existingIndex >= 0) {
updatedSessions = List.of(sessions);
updatedSessions[existingIndex] = session;
} else {
updatedSessions = List.of(sessions)..add(session);
}
// 🔧 修复:确保所有字段都被正确初始化
// 根据编辑原因决定emit的状态类型
if (event.editReason.contains('修改') || event.editReason.contains('编辑')) {
// 编辑模式emit SettingGenerationInProgress状态支持节点修改
emit(SettingGenerationInProgress(
strategies: strategies,
sessions: updatedSessions,
activeSessionId: session.sessionId,
activeSession: session,
currentOperation: '已进入编辑模式',
isGenerating: false,
// 渲染相关字段
nodeRenderStates: const {},
renderedNodeIds: const {},
selectedNodeId: null,
viewMode: 'compact',
adjustmentPrompt: '',
pendingChanges: const {},
highlightedNodeIds: const {},
editHistory: const {},
events: const [],
renderQueue: const [],
));
AppLogger.i(_tag, '✅ 进入编辑模式: ${session.sessionId}, 节点数: ${session.rootNodes.length}');
} else {
// 查看模式emit SettingGenerationCompleted状态
emit(SettingGenerationCompleted(
strategies: strategies,
sessions: updatedSessions,
activeSessionId: session.sessionId,
activeSession: session,
message: '已加载历史设定',
// 🔧 关键修复:历史记录查看模式,所有节点应该显示
nodeRenderStates: const {},
renderedNodeIds: _collectAllNodeIds(session.rootNodes).toSet(),
selectedNodeId: null,
viewMode: 'compact',
adjustmentPrompt: '',
pendingChanges: const {},
highlightedNodeIds: const {},
editHistory: const {},
events: const [],
));
AppLogger.i(_tag, '✅ 查看模式: ${session.sessionId}, 节点数: ${session.rootNodes.length}');
}
AppLogger.i(_tag, '成功加载历史设定: ${session.sessionId}, 节点数: ${session.rootNodes.length}');
} catch (e, stackTrace) {
AppLogger.error(_tag, '加载历史设定失败', e, stackTrace);
// 保留会话列表,避免错误时丢失历史记录
List<SettingGenerationSession> sessions = [];
if (state is SettingGenerationReady) {
sessions = (state as SettingGenerationReady).sessions;
} else if (state is SettingGenerationInProgress) {
sessions = (state as SettingGenerationInProgress).sessions;
} else if (state is SettingGenerationCompleted) {
sessions = (state as SettingGenerationCompleted).sessions;
} else if (state is SettingGenerationError) {
sessions = (state as SettingGenerationError).sessions;
}
emit(SettingGenerationError(
message: '加载历史设定失败:${e.toString()}',
error: e,
stackTrace: stackTrace,
sessions: sessions,
));
}
}
/// 获取会话状态
Future<void> _onGetSessionStatus(
GetSessionStatusEvent event,
Emitter<SettingGenerationState> emit,
) async {
try {
final statusResult = await _repository.getSessionStatus(
sessionId: event.sessionId,
);
// 根据状态更新相应的UI
AppLogger.i(_tag, '会话状态: $statusResult');
} catch (e, stackTrace) {
AppLogger.error(_tag, '获取会话状态失败', e, stackTrace);
}
}
/// 取消会话
Future<void> _onCancelSession(
CancelSessionEvent event,
Emitter<SettingGenerationState> emit,
) async {
try {
await _repository.cancelSession(sessionId: event.sessionId);
// 更新UI状态
if (state is SettingGenerationInProgress) {
final currentState = state as SettingGenerationInProgress;
if (currentState.activeSessionId == event.sessionId) {
emit(currentState.copyWith(
isGenerating: false,
currentOperation: null,
));
}
}
AppLogger.i(_tag, '会话已取消: ${event.sessionId}');
} catch (e, stackTrace) {
AppLogger.error(_tag, '取消会话失败', e, stackTrace);
}
}
/// 获取用户历史记录
Future<void> _onGetUserHistories(
GetUserHistoriesEvent event,
Emitter<SettingGenerationState> emit,
) async {
try {
final histories = await _repository.getUserHistories(
novelId: event.novelId,
page: event.page,
size: event.size,
);
// 转换为Session对象并更新状态
final sessions = histories.map((history) {
return SettingGenerationSession.fromJson(history);
}).toList();
// 根据当前状态更新
if (state is SettingGenerationReady) {
final currentState = state as SettingGenerationReady;
emit(currentState.copyWith(sessions: sessions));
}
AppLogger.i(_tag, '成功获取${sessions.length}条用户历史记录');
} catch (e, stackTrace) {
AppLogger.error(_tag, '获取用户历史记录失败', e, stackTrace);
}
}
/// 删除历史记录
Future<void> _onDeleteHistory(
DeleteHistoryEvent event,
Emitter<SettingGenerationState> emit,
) async {
try {
await _repository.deleteHistory(historyId: event.historyId);
// 从当前会话列表中移除
if (state is SettingGenerationReady) {
final currentState = state as SettingGenerationReady;
final updatedSessions = currentState.sessions
.where((session) => session.sessionId != event.historyId)
.toList();
emit(currentState.copyWith(sessions: updatedSessions));
}
AppLogger.i(_tag, '历史记录已删除: ${event.historyId}');
} catch (e, stackTrace) {
AppLogger.error(_tag, '删除历史记录失败', e, stackTrace);
}
}
/// 复制历史记录
Future<void> _onCopyHistory(
CopyHistoryEvent event,
Emitter<SettingGenerationState> emit,
) async {
try {
final result = await _repository.copyHistory(
historyId: event.historyId,
copyReason: event.copyReason,
);
// 创建新的会话对象并添加到列表
final newSession = SettingGenerationSession.fromJson(result);
if (state is SettingGenerationReady) {
final currentState = state as SettingGenerationReady;
final updatedSessions = [newSession, ...currentState.sessions];
emit(currentState.copyWith(sessions: updatedSessions));
}
AppLogger.i(_tag, '历史记录复制成功');
} catch (e, stackTrace) {
AppLogger.error(_tag, '复制历史记录失败', e, stackTrace);
}
}
/// 恢复历史记录到小说
Future<void> _onRestoreHistoryToNovel(
RestoreHistoryToNovelEvent event,
Emitter<SettingGenerationState> emit,
) async {
try {
emit(const SettingGenerationLoading(message: '正在恢复历史记录...'));
final result = await _repository.restoreHistoryToNovel(
historyId: event.historyId,
novelId: event.novelId,
);
final restoredSettingIds = result['restoredSettingIds'] as List<dynamic>;
emit(SettingGenerationSaved(
savedSettingIds: restoredSettingIds.cast<String>(),
message: '历史记录已成功恢复到小说中',
));
AppLogger.i(_tag, '历史记录恢复成功,恢复了${restoredSettingIds.length}个设定');
} catch (e, stackTrace) {
AppLogger.error(_tag, '恢复历史记录失败', e, stackTrace);
emit(SettingGenerationError(
message: '恢复历史记录失败:${e.toString()}',
error: e,
stackTrace: stackTrace,
));
}
}
/// 更新调整提示词
void _onUpdateAdjustmentPrompt(
UpdateAdjustmentPromptEvent event,
Emitter<SettingGenerationState> emit,
) {
if (state is SettingGenerationReady) {
final currentState = state as SettingGenerationReady;
emit(currentState.copyWith(adjustmentPrompt: event.prompt));
} else if (state is SettingGenerationInProgress) {
final currentState = state as SettingGenerationInProgress;
emit(currentState.copyWith(adjustmentPrompt: event.prompt));
} else if (state is SettingGenerationCompleted) {
final currentState = state as SettingGenerationCompleted;
emit(currentState.copyWith(adjustmentPrompt: event.prompt));
}
}
/// 重置状态
void _onReset(
ResetEvent event,
Emitter<SettingGenerationState> emit,
) {
_generationStreamSubscription?.cancel();
_updateStreamSubscription?.cancel();
emit(const SettingGenerationInitial());
}
/// 重试事件处理(从错误状态恢复)
Future<void> _onRetry(
RetryEvent event,
Emitter<SettingGenerationState> emit,
) async {
try {
AppLogger.i(_tag, '🔄 用户请求重试,重新加载策略');
// 取消任何正在进行的流订阅
_generationStreamSubscription?.cancel();
_updateStreamSubscription?.cancel();
emit(const SettingGenerationLoading(message: '正在重新初始化...'));
// 重新加载策略
final strategies = await _repository.getAvailableStrategies();
emit(SettingGenerationReady(
strategies: strategies,
sessions: [],
));
AppLogger.i(_tag, '✅ 重试成功,系统已重新初始化');
} catch (e, stackTrace) {
AppLogger.error(_tag, '重试失败', e, stackTrace);
emit(SettingGenerationError(
message: '重试失败:${e.toString()}',
error: e,
stackTrace: stackTrace,
isRecoverable: true,
));
}
}
// ==================== 内部事件处理器 ====================
/// 处理生成事件
void _onHandleGenerationEvent(
_HandleGenerationEventInternal event,
Emitter<SettingGenerationState> emit,
) {
// 收到任何后端生成事件都视为"活动",仅在生成/更新中才刷新超时计时
if (state is SettingGenerationInProgress || state is SettingGenerationNodeUpdating) {
_markActivityAndResetTimeout();
}
// 🔧 修复支持SettingGenerationNodeUpdating状态
if (state is! SettingGenerationInProgress && state is! SettingGenerationNodeUpdating) return;
final generationEvent = event.event;
AppLogger.info(_tag, '收到生成事件: ${generationEvent.eventType}');
// 🔧 新增针对SettingGenerationNodeUpdating状态的特殊处理
if (state is SettingGenerationNodeUpdating) {
final currentState = state as SettingGenerationNodeUpdating;
final updatedEvents = [...currentState.events, generationEvent];
// 🔧 移除在新的非删除式修改方案中不再处理NodeDeletedEvent
// if (generationEvent is event_model.NodeDeletedEvent) { ... }
if (generationEvent is event_model.NodeCreatedEvent) {
AppLogger.i(_tag, ' 节点创建事件 (NodeUpdating): ${generationEvent.node.name}');
final updatedNodes = _insertNodeIntoTree(
currentState.activeSession.rootNodes,
generationEvent.node,
generationEvent.parentPath,
);
final updatedSession = currentState.activeSession.copyWith(rootNodes: updatedNodes);
final updatedSessions = currentState.sessions.map((s) => s.sessionId == currentState.activeSessionId ? updatedSession : s).toList();
// 🔧 关键修复将新创建的节点ID添加到renderedNodeIds中使其立即可见
final updatedRenderedNodeIds = Set<String>.from(currentState.renderedNodeIds)
..add(generationEvent.node.id);
// 🔧 添加新节点的渲染状态为已渲染
final updatedNodeRenderStates = Map<String, NodeRenderInfo>.from(currentState.nodeRenderStates);
updatedNodeRenderStates[generationEvent.node.id] = NodeRenderInfo(
nodeId: generationEvent.node.id,
state: NodeRenderState.rendered,
);
AppLogger.i(_tag, '🔄 创建节点后 - 已渲染节点数: ${updatedRenderedNodeIds.length}');
emit(currentState.copyWith(
sessions: updatedSessions,
activeSession: updatedSession,
events: updatedEvents,
message: '已创建节点: ${generationEvent.node.name}',
selectedNodeId: generationEvent.node.id,
renderedNodeIds: updatedRenderedNodeIds,
nodeRenderStates: updatedNodeRenderStates,
));
return;
}
if (generationEvent is event_model.NodeUpdatedEvent) {
// 🔧 关键:只更新特定节点,不触发整个树的重新渲染
AppLogger.i(_tag, '📝 节点修改完成: ${generationEvent.node.name} (ID: ${generationEvent.node.id})');
final updatedNodes = _updateNodeInTree(
currentState.activeSession.rootNodes,
generationEvent.node.id,
generationEvent.node,
);
final updatedSession = currentState.activeSession.copyWith(rootNodes: updatedNodes);
final updatedSessions = currentState.sessions.map((session) {
return session.sessionId == currentState.activeSessionId ? updatedSession : session;
}).toList();
// 🔧 返回到Completed状态表示节点修改完成
emit(SettingGenerationCompleted(
strategies: currentState.strategies,
sessions: updatedSessions,
activeSessionId: currentState.activeSessionId,
activeSession: updatedSession,
selectedNodeId: currentState.selectedNodeId,
viewMode: currentState.viewMode,
adjustmentPrompt: currentState.adjustmentPrompt,
pendingChanges: currentState.pendingChanges,
highlightedNodeIds: const {},
editHistory: currentState.editHistory,
events: [...currentState.events, generationEvent],
message: '节点 "${generationEvent.node.name}" 修改完成',
nodeRenderStates: currentState.nodeRenderStates,
renderedNodeIds: currentState.renderedNodeIds,
));
return;
} else if (generationEvent is event_model.GenerationProgressEvent) {
// 只更新进度消息保持在NodeUpdating状态
emit(currentState.copyWith(
message: generationEvent.message,
events: [...currentState.events, generationEvent],
));
return;
} else if (generationEvent is event_model.GenerationErrorEvent) {
// 节点修改失败不进入错误态保持原态并结束NodeUpdating
emit(currentState.copyWith(
message: '节点修改失败:${generationEvent.errorMessage}',
events: updatedEvents,
// 结束更新中标记
// ignore: invalid_use_of_visible_for_testing_member
isUpdating: false,
));
return;
} else if (generationEvent is event_model.GenerationCompletedEvent) {
// 后端现在会在完成时自然结束SSE流takeUntil + sink.complete不主动取消以避免插件触发 AbortError → 自动重连
// 修改流程完成返回到Completed状态
emit(SettingGenerationCompleted(
strategies: currentState.strategies,
sessions: currentState.sessions,
activeSessionId: currentState.activeSessionId,
activeSession: currentState.activeSession,
selectedNodeId: currentState.selectedNodeId,
viewMode: currentState.viewMode,
adjustmentPrompt: currentState.adjustmentPrompt,
pendingChanges: currentState.pendingChanges,
highlightedNodeIds: const {},
editHistory: currentState.editHistory,
events: [...currentState.events, generationEvent],
message: generationEvent.message,
nodeRenderStates: currentState.nodeRenderStates,
renderedNodeIds: currentState.renderedNodeIds,
));
return;
}
// 其他事件在NodeUpdating状态下忽略或简单更新
emit(currentState.copyWith(
events: [...currentState.events, generationEvent],
));
return;
}
// 原有的SettingGenerationInProgress状态处理逻辑
final currentState = state as SettingGenerationInProgress;
final updatedEvents = [...currentState.events, generationEvent];
// 🔧 移除在新的非删除式修改方案中不再处理NodeDeletedEvent
// if (generationEvent is event_model.NodeDeletedEvent) { ... }
if (generationEvent is event_model.SessionStartedEvent) {
// 🔧 关键修复更新为后端返回的真实sessionID
final realSessionId = generationEvent.sessionId;
AppLogger.i(_tag, '🔄 更新sessionID: ${currentState.activeSessionId} -> $realSessionId');
// 更新会话信息
final updatedSession = currentState.activeSession.copyWith(
sessionId: realSessionId,
);
final updatedSessions = currentState.sessions.map((session) {
return session.sessionId == currentState.activeSessionId ? updatedSession : session;
}).toList();
emit(currentState.copyWith(
events: updatedEvents,
currentOperation: '会话已启动,正在生成设定...',
activeSessionId: realSessionId, // 🔧 更新活跃会话ID
activeSession: updatedSession, // 🔧 更新活跃会话对象
sessions: updatedSessions, // 🔧 更新会话列表
));
} else if (generationEvent is event_model.NodeCreatedEvent) {
// 🚀 改进:智能立即处理,只有真正需要等待的节点才暂存
AppLogger.i(_tag, '⚡ 智能处理节点: ${generationEvent.node.name}');
// 使用智能处理逻辑
_processNodesImmediately([generationEvent], emit);
} else if (generationEvent is event_model.NodeUpdatedEvent) {
final updatedNodes = _updateNodeInTree(
currentState.activeSession.rootNodes,
generationEvent.node.id,
generationEvent.node,
);
final updatedSession = currentState.activeSession.copyWith(rootNodes: updatedNodes);
final updatedSessions = currentState.sessions.map((session) {
return session.sessionId == currentState.activeSessionId ? updatedSession : session;
}).toList();
emit(currentState.copyWith(
sessions: updatedSessions,
activeSession: updatedSession,
events: updatedEvents,
currentOperation: '已更新节点: ${generationEvent.node.name}',
));
} else if (generationEvent is event_model.GenerationProgressEvent) {
// 只更新操作消息避免频繁更新events数组
if (currentState.currentOperation != generationEvent.message) {
emit(currentState.copyWith(
currentOperation: generationEvent.message,
));
}
} else if (generationEvent is event_model.GenerationCompletedEvent) {
// 🔧 关键修复:在完成前,强制处理所有暂存的节点
if (currentState.pendingNodes.isNotEmpty) {
// 后端会自然结束SSE避免主动取消导致 AbortError
AppLogger.i(_tag, '⚡️ 完成信号收到,强制处理 ${currentState.pendingNodes.length} 个暂存节点');
// 🚀 改进:使用智能处理替代原有的批量处理
final allPendingNodes = List<event_model.NodeCreatedEvent>.from(currentState.pendingNodes);
_processNodesImmediately(allPendingNodes, emit);
// 等待一小段时间确保所有节点都被处理
Timer(const Duration(milliseconds: 100), () {
if (!isClosed) {
add(const ProcessRenderQueueEvent());
}
});
// 重新获取最新的状态
final latestState = state as SettingGenerationInProgress;
// 使用最新的状态继续完成流程
final updatedSession = latestState.activeSession.copyWith(status: SessionStatus.completed);
final updatedSessions = latestState.sessions.map((session) {
return session.sessionId == latestState.activeSessionId ? updatedSession : session;
}).toList();
// 🔧 关键修复:将所有正在渲染的节点标记为已渲染,避免状态转换时丢失
final renderingNodeIds = latestState.nodeRenderStates.entries
.where((entry) => entry.value.state == NodeRenderState.rendering)
.map((entry) => entry.key)
.toSet();
final finalRenderedNodeIds = Set<String>.from(latestState.renderedNodeIds)
..addAll(renderingNodeIds);
AppLogger.i(_tag, '🔧 完成时强制标记正在渲染的节点为已渲染: ${renderingNodeIds.length}个, 总已渲染: ${finalRenderedNodeIds.length}');
emit(SettingGenerationCompleted(
strategies: latestState.strategies,
sessions: updatedSessions,
activeSessionId: latestState.activeSessionId,
activeSession: updatedSession,
message: generationEvent.message,
// 🔧 关键修复:使用包含正在渲染节点的完整集合
nodeRenderStates: latestState.nodeRenderStates,
renderedNodeIds: finalRenderedNodeIds,
));
} else {
// 正常完成流程:先 flush 所有待处理节点,再触发渲染队列,然后统一收尾
// 1) Flush pendingNodes如有
if (currentState.pendingNodes.isNotEmpty) {
AppLogger.i(_tag, '⚡️ 正常完成前先处理 ${currentState.pendingNodes.length} 个暂存节点');
final allPendingNodes = List<event_model.NodeCreatedEvent>.from(currentState.pendingNodes);
_processNodesImmediately(allPendingNodes, emit);
// 触发一次渲染队列处理
Timer(const Duration(milliseconds: 50), () {
if (!isClosed) {
add(const ProcessRenderQueueEvent());
}
});
}
// 2) 统一把 pending/queued/rendering 的节点标记为已渲染,并确保插入到树
final latest = state as SettingGenerationInProgress; // flush 后取最新
final updatedSession = latest.activeSession.copyWith(status: SessionStatus.completed);
final updatedSessions = latest.sessions.map((session) {
return session.sessionId == latest.activeSessionId ? updatedSession : session;
}).toList();
// 收集需要标记完成的节点ID非已渲染
final toFinalizeIds = latest.nodeRenderStates.entries
.where((e) => e.value.state == NodeRenderState.pending ||
e.value.state == NodeRenderState.rendering)
.map((e) => e.key)
.where((id) => !latest.renderedNodeIds.contains(id))
.toSet();
// 将这些节点加入 rendered 集合
final finalRenderedNodeIds = Set<String>.from(latest.renderedNodeIds)..addAll(toFinalizeIds);
AppLogger.i(_tag, '🔧 正常完成:补标记未完成节点为已渲染: ${toFinalizeIds.length} 个, 总已渲染: ${finalRenderedNodeIds.length}');
emit(SettingGenerationCompleted(
strategies: latest.strategies,
sessions: updatedSessions,
activeSessionId: latest.activeSessionId,
activeSession: updatedSession,
selectedNodeId: latest.selectedNodeId,
viewMode: latest.viewMode,
adjustmentPrompt: latest.adjustmentPrompt,
pendingChanges: latest.pendingChanges,
highlightedNodeIds: const {},
editHistory: latest.editHistory,
events: updatedEvents,
message: generationEvent.message,
nodeRenderStates: latest.nodeRenderStates,
renderedNodeIds: finalRenderedNodeIds,
));
}
} else if (generationEvent is event_model.GenerationErrorEvent) {
// 保留当前 UI不切换到 Error 状态,只停止生成并记录错误
emit(currentState.copyWith(
isGenerating: false,
currentOperation: null,
events: updatedEvents,
));
return;
}
}
/// 处理生成错误
void _onHandleGenerationError(
_HandleGenerationErrorInternal event,
Emitter<SettingGenerationState> emit,
) {
String message = event.userFriendlyMessage ?? _getUserFriendlyErrorMessage(event.error);
// 🔧 新增发生错误时立即取消生成流避免SSE自动重连导致无限重试
_generationStreamSubscription?.cancel();
// 优先处理超时:不切换到错误页,保留当前设定树,仅在顶部显示状态
final errorString = event.error.toString().toLowerCase();
final isTimeout = errorString.contains('timeout');
if (isTimeout) {
if (state is SettingGenerationInProgress) {
final currentState = state as SettingGenerationInProgress;
emit(currentState.copyWith(
isGenerating: false,
currentOperation: '请求超时,连接已断开。已保留当前设定内容,可稍后重试',
));
return;
}
if (state is SettingGenerationCompleted) {
// Completed 状态无 currentOperation 字段,仅提示即可,不改变状态
return;
}
if (state is SettingGenerationReady) {
final currentState = state as SettingGenerationReady;
emit(currentState.copyWith(
// Ready 状态下,仅提示,不破坏会话与策略
));
return;
}
}
List<SettingGenerationSession> sessions = [];
String? activeSessionId;
if (state is SettingGenerationReady) {
sessions = (state as SettingGenerationReady).sessions;
activeSessionId = (state as SettingGenerationReady).activeSessionId;
// 🔧 Ready 状态下也确保停止生成标志
emit((state as SettingGenerationReady).copyWith());
} else if (state is SettingGenerationInProgress) {
sessions = (state as SettingGenerationInProgress).sessions;
activeSessionId = (state as SettingGenerationInProgress).activeSessionId;
// 🔧 确保停止生成并清空进度文案
final currentState = state as SettingGenerationInProgress;
emit(currentState.copyWith(
isGenerating: false,
currentOperation: null,
));
} else if (state is SettingGenerationCompleted) {
sessions = (state as SettingGenerationCompleted).sessions;
activeSessionId = (state as SettingGenerationCompleted).activeSessionId;
}
// 在 NodeUpdating 期间保持原态并弹Toast不进入错误态
if (state is SettingGenerationNodeUpdating) {
add(const _HandleGenerationCompleteInternal());
return;
}
emit(SettingGenerationError(
message: message,
error: event.error,
stackTrace: event.stackTrace,
isRecoverable: _isRecoverableError(event.error),
sessions: sessions,
activeSessionId: activeSessionId,
));
}
/// 处理生成完成
void _onHandleGenerationComplete(
_HandleGenerationCompleteInternal event,
Emitter<SettingGenerationState> emit,
) {
if (state is SettingGenerationInProgress) {
final currentState = state as SettingGenerationInProgress;
emit(currentState.copyWith(
isGenerating: false,
currentOperation: null,
));
}
}
/// 🚀 改进的渲染队列处理
void _onProcessRenderQueue(
ProcessRenderQueueEvent event,
Emitter<SettingGenerationState> emit,
) {
if (state is! SettingGenerationInProgress) return;
final currentState = state as SettingGenerationInProgress;
// 🚀 实时计算可渲染节点不依赖过时的renderableNodeIds
final renderableNodeIds = _calculateRenderableNodesEfficiently(
currentState.activeSession.rootNodes,
currentState.renderQueue,
currentState.renderedNodeIds,
currentState.nodeRenderStates,
);
AppLogger.i(_tag, '🚀 实时计算渲染队列,可渲染节点: ${renderableNodeIds.length}');
// 过滤掉已经在渲染中或已渲染的节点
final nodesToRender = renderableNodeIds.where((nodeId) {
final renderInfo = currentState.nodeRenderStates[nodeId];
final isAlreadyProcessing = renderInfo?.state == NodeRenderState.rendering;
final isAlreadyRendered = currentState.renderedNodeIds.contains(nodeId);
return !isAlreadyProcessing && !isAlreadyRendered;
}).toList();
if (nodesToRender.isEmpty) {
AppLogger.i(_tag, '📝 没有新的节点需要渲染');
return;
}
// 🔧 关键修复:按父节点分组,避免同一父节点的子节点同时渲染
final nodesByParent = <String?, List<String>>{};
for (final nodeId in nodesToRender) {
final node = SettingNodeUtils.findNodeInTree(currentState.activeSession.rootNodes, nodeId);
if (node != null) {
final parentNode = SettingNodeUtils.findParentNodeInTree(currentState.activeSession.rootNodes, nodeId);
final parentKey = parentNode?.id ?? 'root';
nodesByParent.putIfAbsent(parentKey, () => []).add(nodeId);
}
}
AppLogger.i(_tag, '🚀 按父节点分组: ${nodesByParent.length}个父节点组');
// 🔧 交错渲染策略:每个父节点组只渲染第一个子节点,其余延迟处理
final immediateNodes = <String>[];
final delayedNodes = <String>[];
for (final entry in nodesByParent.entries) {
final parentKey = entry.key;
final childNodes = entry.value;
if (childNodes.isNotEmpty) {
// 每个父节点组立即渲染第一个子节点
immediateNodes.add(childNodes.first);
AppLogger.i(_tag, '⚡ 立即渲染: ${childNodes.first} (父节点: $parentKey)');
// 其余子节点延迟渲染
if (childNodes.length > 1) {
delayedNodes.addAll(childNodes.skip(1));
AppLogger.i(_tag, '⏰ 延迟渲染: ${childNodes.skip(1).join(', ')} (父节点: $parentKey)');
}
}
}
// 🔧 批量更新立即渲染的节点状态
if (immediateNodes.isNotEmpty) {
final updatedNodeRenderStates = Map<String, NodeRenderInfo>.from(currentState.nodeRenderStates);
final updatedRenderQueue = currentState.renderQueue.where((id) => !immediateNodes.contains(id)).toList();
final updatedHighlightedNodeIds = Set<String>.from(currentState.highlightedNodeIds);
// 为立即渲染的节点设置渲染状态
for (final nodeId in immediateNodes) {
updatedNodeRenderStates[nodeId] = NodeRenderInfo(
nodeId: nodeId,
state: NodeRenderState.rendering,
renderStartTime: DateTime.now(),
);
updatedHighlightedNodeIds.add(nodeId);
AppLogger.i(_tag, '▶️ 开始渲染节点: $nodeId');
// 🔧 设置完成渲染的定时器
Timer(const Duration(milliseconds: 800), () {
if (!isClosed) {
AppLogger.i(_tag, '⏰ 触发节点渲染完成事件: $nodeId');
add(CompleteNodeRenderEvent(nodeId));
} else {
AppLogger.w(_tag, '⚠️ BLoC已关闭跳过节点渲染完成: $nodeId');
}
});
}
emit(currentState.copyWith(
nodeRenderStates: updatedNodeRenderStates,
renderQueue: updatedRenderQueue,
highlightedNodeIds: updatedHighlightedNodeIds,
));
}
// 🔧 延迟处理其余节点,避免同一帧内多次状态变化
if (delayedNodes.isNotEmpty) {
Timer(const Duration(milliseconds: 200), () {
if (!isClosed && state is SettingGenerationInProgress) {
// 直接触发队列处理事件,让渲染队列自然处理延迟节点
add(const ProcessRenderQueueEvent());
}
});
}
}
// 🔧 修复:简化开始渲染节点的逻辑
void _onStartNodeRender(
StartNodeRenderEvent event,
Emitter<SettingGenerationState> emit,
) {
if (state is! SettingGenerationInProgress) return;
final currentState = state as SettingGenerationInProgress;
final nodeId = event.nodeId;
// 🔧 修复:检查节点是否已经在处理中,避免重复处理
final renderInfo = currentState.nodeRenderStates[nodeId];
if (renderInfo?.state == NodeRenderState.rendering ||
currentState.renderedNodeIds.contains(nodeId)) {
AppLogger.w(_tag, '⚠️ 节点已在处理中,跳过: $nodeId');
return;
}
// 更新节点渲染状态为正在渲染
final updatedNodeRenderStates = Map<String, NodeRenderInfo>.from(currentState.nodeRenderStates);
updatedNodeRenderStates[nodeId] = NodeRenderInfo(
nodeId: nodeId,
state: NodeRenderState.rendering,
renderStartTime: DateTime.now(),
);
// 从渲染队列中移除
final updatedRenderQueue = currentState.renderQueue.where((id) => id != nodeId).toList();
// 添加到高亮列表
final updatedHighlightedNodeIds = Set<String>.from(currentState.highlightedNodeIds)..add(nodeId);
emit(currentState.copyWith(
nodeRenderStates: updatedNodeRenderStates,
renderQueue: updatedRenderQueue,
highlightedNodeIds: updatedHighlightedNodeIds,
));
AppLogger.i(_tag, '▶️ 开始渲染节点: $nodeId');
// 设置定时器自动完成渲染(模拟动画时间)
Timer(const Duration(milliseconds: 800), () {
if (!isClosed) {
AppLogger.i(_tag, '⏰ 触发节点渲染完成事件: $nodeId');
add(CompleteNodeRenderEvent(nodeId));
} else {
AppLogger.w(_tag, '⚠️ BLoC已关闭跳过节点渲染完成: $nodeId');
}
});
}
// 🔧 修复:完成节点渲染,支持多种状态
void _onCompleteNodeRender(
CompleteNodeRenderEvent event,
Emitter<SettingGenerationState> emit,
) {
AppLogger.i(_tag, '🔄 处理节点渲染完成事件: ${event.nodeId}');
final nodeId = event.nodeId;
// 🔧 关键修复支持SettingGenerationInProgress和SettingGenerationCompleted两种状态
if (state is SettingGenerationInProgress) {
final currentState = state as SettingGenerationInProgress;
_completeNodeRenderInProgress(currentState, nodeId, emit);
} else if (state is SettingGenerationCompleted) {
final currentState = state as SettingGenerationCompleted;
_completeNodeRenderInCompleted(currentState, nodeId, emit);
} else {
AppLogger.w(_tag, '⚠️ 状态不支持渲染完成: ${event.nodeId} (当前状态: ${state.runtimeType})');
}
}
/// 在InProgress状态下完成节点渲染
void _completeNodeRenderInProgress(
SettingGenerationInProgress currentState,
String nodeId,
Emitter<SettingGenerationState> emit,
) {
// 🔧 修复:检查节点是否已经完成渲染,避免重复处理
if (currentState.renderedNodeIds.contains(nodeId)) {
AppLogger.w(_tag, '⚠️ 节点已完成渲染,跳过: $nodeId');
return;
}
// 更新节点渲染状态为已渲染
final updatedNodeRenderStates = Map<String, NodeRenderInfo>.from(currentState.nodeRenderStates);
updatedNodeRenderStates[nodeId] = NodeRenderInfo(
nodeId: nodeId,
state: NodeRenderState.rendered,
renderStartTime: updatedNodeRenderStates[nodeId]?.renderStartTime,
renderDuration: updatedNodeRenderStates[nodeId]?.renderStartTime != null
? DateTime.now().difference(updatedNodeRenderStates[nodeId]!.renderStartTime!)
: null,
);
// 添加到已渲染节点集合
final beforeCount = currentState.renderedNodeIds.length;
final updatedRenderedNodeIds = Set<String>.from(currentState.renderedNodeIds)..add(nodeId);
final afterCount = updatedRenderedNodeIds.length;
AppLogger.i(_tag, '📊 更新已渲染节点集合: $nodeId (${beforeCount} -> ${afterCount})');
// 从高亮列表中移除
final updatedHighlightedNodeIds = Set<String>.from(currentState.highlightedNodeIds)..remove(nodeId);
emit(currentState.copyWith(
nodeRenderStates: updatedNodeRenderStates,
renderedNodeIds: updatedRenderedNodeIds,
highlightedNodeIds: updatedHighlightedNodeIds,
));
AppLogger.i(_tag, '✅ 完成渲染节点: $nodeId, 总已渲染: ${afterCount}');
// 🔧 修复使用更长的延迟处理确保UI稳定后再处理下一批
Timer(const Duration(milliseconds: 300), () {
if (!isClosed && state is SettingGenerationInProgress) {
final current = state as SettingGenerationInProgress;
// 只有当还有队列中的节点时才继续处理
if (current.renderQueue.isNotEmpty) {
add(const ProcessRenderQueueEvent());
}
}
});
}
/// 在Completed状态下完成节点渲染
void _completeNodeRenderInCompleted(
SettingGenerationCompleted currentState,
String nodeId,
Emitter<SettingGenerationState> emit,
) {
AppLogger.i(_tag, '🔄 在Completed状态下处理节点渲染完成: $nodeId');
// 🔧 检查节点是否已经完成渲染
if (currentState.renderedNodeIds.contains(nodeId)) {
AppLogger.w(_tag, '⚠️ 节点已完成渲染,跳过: $nodeId');
return;
}
// 🔧 关键修复在Completed状态下也要更新renderedNodeIds
final beforeCount = currentState.renderedNodeIds.length;
final updatedRenderedNodeIds = Set<String>.from(currentState.renderedNodeIds)..add(nodeId);
final afterCount = updatedRenderedNodeIds.length;
AppLogger.i(_tag, '📊 Completed状态下更新已渲染节点集合: $nodeId (${beforeCount} -> ${afterCount})');
// 更新节点渲染状态
final updatedNodeRenderStates = Map<String, NodeRenderInfo>.from(currentState.nodeRenderStates);
updatedNodeRenderStates[nodeId] = NodeRenderInfo(
nodeId: nodeId,
state: NodeRenderState.rendered,
renderStartTime: updatedNodeRenderStates[nodeId]?.renderStartTime,
renderDuration: updatedNodeRenderStates[nodeId]?.renderStartTime != null
? DateTime.now().difference(updatedNodeRenderStates[nodeId]!.renderStartTime!)
: null,
);
// 🔧 发出更新后的Completed状态
emit(SettingGenerationCompleted(
strategies: currentState.strategies,
sessions: currentState.sessions,
activeSessionId: currentState.activeSessionId,
activeSession: currentState.activeSession,
selectedNodeId: currentState.selectedNodeId,
viewMode: currentState.viewMode,
adjustmentPrompt: currentState.adjustmentPrompt,
pendingChanges: currentState.pendingChanges,
highlightedNodeIds: currentState.highlightedNodeIds,
editHistory: currentState.editHistory,
events: currentState.events,
message: currentState.message,
nodeRenderStates: updatedNodeRenderStates,
renderedNodeIds: updatedRenderedNodeIds,
));
AppLogger.i(_tag, '✅ Completed状态下完成渲染节点: $nodeId, 总已渲染: ${afterCount}');
}
// 工具方法
// 🔧 移除_removeNodesFromTree函数不再使用非删除式修改方案
/// 将新节点插入到树中的正确位置(支持层级结构)
List<SettingNode> _insertNodeIntoTree(
List<SettingNode> nodes,
SettingNode newNode,
String? parentPath,
) {
// 如果没有父路径,添加到根级别
if (parentPath == null || parentPath.isEmpty) {
AppLogger.i(_tag, '🌳 ${newNode.name} -> 根节点');
return [...nodes, newNode];
}
// 处理路径:移除开头的/然后split
String cleanPath = parentPath.startsWith('/') ? parentPath.substring(1) : parentPath;
if (cleanPath.isEmpty) {
AppLogger.i(_tag, '🌳 ${newNode.name} -> 根节点');
return [...nodes, newNode];
}
final pathSegments = cleanPath.split('/').where((segment) => segment.isNotEmpty).toList();
AppLogger.i(_tag, '🌳 ${newNode.name} -> ${pathSegments.join('/')}');
// 根据父路径查找正确的插入位置
final result = _insertNodeAtPath(nodes, newNode, pathSegments);
return result;
}
/// 递归插入节点到指定路径
List<SettingNode> _insertNodeAtPath(
List<SettingNode> nodes,
SettingNode newNode,
List<String> pathSegments,
) {
if (pathSegments.isEmpty) {
// 🔧 性能优化:若已存在同 id 节点,直接替换而非重复插入
final existingIndex = nodes.indexWhere((n) => n.id == newNode.id);
if (existingIndex != -1) {
final replaced = [...nodes];
replaced[existingIndex] = newNode;
return replaced;
}
return [...nodes, newNode];
}
final targetName = pathSegments.first;
final remainingPath = pathSegments.skip(1).toList();
// 先尝试按ID查找如果找不到则按名称查找
SettingNode? targetNode;
int targetIndex = -1;
for (int i = 0; i < nodes.length; i++) {
final node = nodes[i];
if (node.id == targetName || node.name == targetName) {
targetNode = node;
targetIndex = i;
break;
}
}
// 如果找不到父节点,创建一个占位父节点
if (targetNode == null) {
AppLogger.w(_tag, '🌳 创建占位父节点: $targetName');
final placeholderParent = SettingNode(
id: 'placeholder_${targetName}_${DateTime.now().millisecondsSinceEpoch}',
name: targetName,
type: SettingType.lore, // 默认类型
description: '占位节点,等待后续更新',
generationStatus: GenerationStatus.pending,
children: [],
);
// 将占位父节点添加到当前级别
final updatedNodes = [...nodes, placeholderParent];
targetNode = placeholderParent;
targetIndex = updatedNodes.length - 1;
// 继续处理剩余路径
if (remainingPath.isEmpty) {
// 这是目标父节点,添加子节点
final currentChildren = targetNode.children ?? [];
// 去重:如果子节点已存在则替换
int existingChildIndex = currentChildren.indexWhere((c) => c.id == newNode.id);
List<SettingNode> updatedChildren;
if (existingChildIndex != -1) {
updatedChildren = [...currentChildren];
updatedChildren[existingChildIndex] = newNode;
} else {
updatedChildren = [...currentChildren, newNode];
}
final updatedNode = targetNode.copyWith(children: updatedChildren);
// 替换原节点
final finalNodes = [...updatedNodes];
finalNodes[targetIndex] = updatedNode;
return finalNodes;
} else {
// 继续向下递归
final updatedChildren = _insertNodeAtPath(
targetNode.children ?? [],
newNode,
remainingPath,
);
final updatedNode = targetNode.copyWith(children: updatedChildren);
// 替换原节点
final finalNodes = [...updatedNodes];
finalNodes[targetIndex] = updatedNode;
return finalNodes;
}
}
if (remainingPath.isEmpty) {
// 这是目标父节点,添加子节点
final currentChildren = targetNode.children ?? [];
// 去重:如果子节点已存在则替换
int existingChildIndex = currentChildren.indexWhere((c) => c.id == newNode.id);
List<SettingNode> updatedChildren;
if (existingChildIndex != -1) {
updatedChildren = [...currentChildren];
updatedChildren[existingChildIndex] = newNode;
} else {
updatedChildren = [...currentChildren, newNode];
}
final updatedNode = targetNode.copyWith(children: updatedChildren);
// 替换原节点
final updatedNodes = [...nodes];
updatedNodes[targetIndex] = updatedNode;
return updatedNodes;
} else {
// 继续向下递归
final updatedChildren = _insertNodeAtPath(
targetNode.children ?? [],
newNode,
remainingPath,
);
final updatedNode = targetNode.copyWith(children: updatedChildren);
// 替换原节点
final updatedNodes = [...nodes];
updatedNodes[targetIndex] = updatedNode;
return updatedNodes;
}
}
/// 更新节点树中的节点
List<SettingNode> _updateNodeInTree(
List<SettingNode> nodes,
String nodeId,
SettingNode updatedNode,
) {
return nodes.map((node) {
if (node.id == nodeId) {
return updatedNode;
}
if (node.children != null) {
return node.copyWith(
children: _updateNodeInTree(node.children!, nodeId, updatedNode),
);
}
return node;
}).toList();
}
/// 应用更改到节点树
List<SettingNode> _applyChangesToNodes(
List<SettingNode> nodes,
Map<String, SettingNode> changes,
) {
return nodes.map((node) {
final updatedNode = changes[node.id] ?? node;
if (node.children != null) {
return updatedNode.copyWith(
children: _applyChangesToNodes(node.children!, changes),
);
}
return updatedNode;
}).toList();
}
/// 获取用户友好的错误信息
String _getUserFriendlyErrorMessage(dynamic error) {
final errorString = error.toString().toLowerCase();
if (errorString.contains('unknown strategy')) {
return '选择的生成策略不可用,请刷新页面后重试';
} else if (errorString.contains('text_stage_empty') || errorString.contains('start_failed')) {
// 明确提示当前模型调用异常
return '当前模型调用异常,请更换模型或稍后重试';
} else if (errorString.contains('network') || errorString.contains('connection')) {
return '网络连接失败,请检查网络后重试';
} else if (errorString.contains('timeout')) {
return '请求超时,请稍后重试';
} else if (errorString.contains('unauthorized') || errorString.contains('forbidden')) {
return '没有权限执行该操作,请检查登录状态';
} else if (errorString.contains('model') || errorString.contains('config')) {
return 'AI模型配置错误请检查模型设置';
} else if (errorString.contains('rate limit') || errorString.contains('quota')) {
return 'AI服务调用频繁请稍后再试';
} else {
return '生成过程中出现错误,请稍后重试';
}
}
/// 判断错误是否可恢复
bool _isRecoverableError(dynamic error) {
final errorString = error.toString().toLowerCase();
// 不可恢复的错误
if (errorString.contains('unauthorized') ||
errorString.contains('forbidden') ||
errorString.contains('invalid model') ||
errorString.contains('configuration error')) {
return false;
}
// 其他错误都认为可恢复
return true;
}
Future<void> _onProcessPendingNodes(
_ProcessPendingNodes event,
Emitter<SettingGenerationState> emit,
) async {
if (state is! SettingGenerationInProgress) return;
final currentState = state as SettingGenerationInProgress;
if (currentState.pendingNodes.isEmpty) return;
AppLogger.i(_tag, '🔄 处理暂存的 ${currentState.pendingNodes.length} 个节点');
var currentNodes = currentState.activeSession.rootNodes;
var updatedRenderQueue = List<String>.from(currentState.renderQueue);
var updatedNodeRenderStates = Map<String, NodeRenderInfo>.from(currentState.nodeRenderStates);
// 1. 拓扑排序
final sortedEvents = _topologicallySortNodes(currentState.pendingNodes);
// 2. 批量插入
for (final nodeEvent in sortedEvents) {
currentNodes = _insertNodeIntoTree(
currentNodes,
nodeEvent.node,
nodeEvent.parentPath,
);
updatedRenderQueue.add(nodeEvent.node.id);
updatedNodeRenderStates[nodeEvent.node.id] = NodeRenderInfo(
nodeId: nodeEvent.node.id,
state: NodeRenderState.pending,
);
}
final updatedSession = currentState.activeSession.copyWith(rootNodes: currentNodes);
final updatedSessions = currentState.sessions.map((session) {
return session.sessionId == currentState.activeSessionId ? updatedSession : session;
}).toList();
emit(currentState.copyWith(
sessions: updatedSessions,
activeSession: updatedSession,
pendingNodes: [], // Clear pending nodes
renderQueue: updatedRenderQueue,
nodeRenderStates: updatedNodeRenderStates,
currentOperation: '已处理 ${sortedEvents.length} 个新节点',
));
// 触发渲染队列处理
add(const ProcessRenderQueueEvent());
}
/// 🚀 改进的拓扑排序算法,支持增量处理和已存在的节点
List<event_model.NodeCreatedEvent> _improvedTopologicalSort(
List<event_model.NodeCreatedEvent> events,
List<SettingNode> existingNodes,
) {
if (events.isEmpty) return [];
final nodes = events.map((e) => e.node).toList();
final nodeMap = {for (var node in nodes) node.id: node};
final eventMap = {for (var e in events) e.node.id: e};
// 构建已存在节点的ID集合
final existingNodeIds = _collectAllNodeIds(existingNodes).toSet();
AppLogger.i(_tag, '🔄 拓扑排序 - 新节点: ${nodes.length}, 已存在: ${existingNodeIds.length}');
// 计算入度,考虑已存在的节点
final inDegree = {for (var node in nodes) node.id: 0};
final graph = {for (var node in nodes) node.id: <String>[]};
// 构建依赖图
for (final node in nodes) {
final parentId = node.parentId;
if (parentId != null) {
if (nodeMap.containsKey(parentId)) {
// 父节点在当前批次中
graph[parentId]!.add(node.id);
inDegree[node.id] = (inDegree[node.id] ?? 0) + 1;
AppLogger.i(_tag, '📊 依赖关系: ${node.name} <- ${nodeMap[parentId]!.name}');
} else if (existingNodeIds.contains(parentId)) {
// 父节点已存在,无需等待
AppLogger.i(_tag, '✅ 父节点已存在: ${node.name}');
// 入度保持为0可以立即处理
} else {
// 父节点既不在当前批次,也不存在,设置高入度等待
inDegree[node.id] = 999;
AppLogger.w(_tag, '❌ 父节点不存在: ${node.name} (需要: $parentId)');
}
}
}
// Kahn算法进行拓扑排序
final queue = inDegree.entries
.where((entry) => entry.value == 0)
.map((entry) => entry.key)
.toList();
final sortedIds = <String>[];
final processedIds = <String>{};
AppLogger.i(_tag, '🚀 开始排序,初始可处理: ${queue.length} 个节点');
while (queue.isNotEmpty) {
final nodeId = queue.removeAt(0);
if (processedIds.contains(nodeId)) {
continue; // 避免重复处理
}
sortedIds.add(nodeId);
processedIds.add(nodeId);
final nodeName = nodeMap[nodeId]?.name ?? nodeId;
AppLogger.i(_tag, '✅ 排序节点: $nodeName');
// 更新依赖此节点的其他节点
if (graph.containsKey(nodeId)) {
for (final neighborId in graph[nodeId]!) {
if (!processedIds.contains(neighborId)) {
inDegree[neighborId] = (inDegree[neighborId] ?? 0) - 1;
if (inDegree[neighborId] == 0) {
queue.add(neighborId);
AppLogger.i(_tag, '➡️ 解锁节点: ${nodeMap[neighborId]?.name ?? neighborId}');
}
}
}
}
}
// 返回排序后的事件,过滤掉无法排序的节点
final sortedEvents = sortedIds
.map((id) => eventMap[id])
.where((e) => e != null)
.cast<event_model.NodeCreatedEvent>()
.toList();
// 检查是否有无法排序的节点(循环依赖或缺少父节点)
final missedNodes = nodes.where((node) => !processedIds.contains(node.id)).toList();
if (missedNodes.isNotEmpty) {
AppLogger.w(_tag, '⚠️ 无法排序的节点: ${missedNodes.map((n) => n.name).join(', ')}');
}
AppLogger.i(_tag, '🎯 排序完成: ${sortedEvents.length}/${nodes.length} 个节点');
return sortedEvents;
}
/// 收集所有节点的ID包括子节点
List<String> _collectAllNodeIds(List<SettingNode> nodes) {
final ids = <String>[];
for (final node in nodes) {
ids.add(node.id);
if (node.children != null) {
ids.addAll(_collectAllNodeIds(node.children!));
}
}
return ids;
}
/// 🚀 高效计算可渲染节点
List<String> _calculateRenderableNodesEfficiently(
List<SettingNode> rootNodes,
List<String> renderQueue,
Set<String> renderedNodeIds,
Map<String, NodeRenderInfo> nodeRenderStates,
) {
final List<String> renderable = [];
AppLogger.i(_tag, '🔍 快速检查 ${renderQueue.length} 个待渲染节点,已渲染: ${renderedNodeIds.length}');
for (final nodeId in renderQueue) {
// 跳过已渲染或正在渲染的节点
if (renderedNodeIds.contains(nodeId)) {
continue;
}
final renderInfo = nodeRenderStates[nodeId];
if (renderInfo?.state == NodeRenderState.rendering) {
continue;
}
final node = SettingNodeUtils.findNodeInTree(rootNodes, nodeId);
if (node == null) {
AppLogger.w(_tag, '❌ 渲染队列中的节点不存在: $nodeId');
continue;
}
// 检查依赖关系
final parentNode = SettingNodeUtils.findParentNodeInTree(rootNodes, nodeId);
if (parentNode == null) {
// 根节点,可以渲染
AppLogger.i(_tag, '✅ 根节点可渲染: ${node.name}');
renderable.add(nodeId);
} else if (renderedNodeIds.contains(parentNode.id)) {
// 父节点已渲染,子节点可以渲染
AppLogger.i(_tag, '✅ 父节点已渲染,可渲染: ${node.name}');
renderable.add(nodeId);
} else {
AppLogger.i(_tag, '⏳ 等待父节点: ${node.name} <- ${parentNode.name}');
}
}
AppLogger.i(_tag, '🎯 高效计算完成: ${renderable.length} 个节点可立即渲染');
return renderable;
}
/// 🔧 保留原有的拓扑排序方法作为备用
List<event_model.NodeCreatedEvent> _topologicallySortNodes(List<event_model.NodeCreatedEvent> events) {
final nodes = events.map((e) => e.node).toList();
final nodeMap = {for (var node in nodes) node.id: node};
final inDegree = {for (var node in nodes) node.id: 0};
final graph = {for (var node in nodes) node.id: <String>[]};
for (final node in nodes) {
// 修正使用node.parentId而不是从parentPath解析
final parentId = node.parentId;
if (parentId != null && nodeMap.containsKey(parentId)) {
graph[parentId]!.add(node.id);
inDegree[node.id] = (inDegree[node.id] ?? 0) + 1;
}
}
final queue = inDegree.entries
.where((entry) => entry.value == 0)
.map((entry) => entry.key)
.toList();
final sortedIds = <String>[];
while (queue.isNotEmpty) {
final nodeId = queue.removeAt(0);
sortedIds.add(nodeId);
if (graph.containsKey(nodeId)) {
for (final neighborId in graph[nodeId]!) {
inDegree[neighborId] = (inDegree[neighborId] ?? 0) - 1;
if (inDegree[neighborId] == 0) {
queue.add(neighborId);
}
}
}
}
final eventMap = {for (var e in events) e.node.id: e};
// 过滤掉可能因父节点不在当前批次而无法排序的节点
return sortedIds.map((id) => eventMap[id]).where((e) => e != null).cast<event_model.NodeCreatedEvent>().toList();
}
}
// 内部事件类
/// 处理生成事件
class _HandleGenerationEventInternal extends SettingGenerationBlocEvent {
final event_model.SettingGenerationEvent event;
const _HandleGenerationEventInternal(this.event);
@override
List<Object?> get props => [event];
}
/// 处理生成错误
class _HandleGenerationErrorInternal extends SettingGenerationBlocEvent {
final dynamic error;
final StackTrace? stackTrace;
final String? userFriendlyMessage;
const _HandleGenerationErrorInternal(this.error, this.stackTrace, [this.userFriendlyMessage]);
@override
List<Object?> get props => [error, stackTrace, userFriendlyMessage];
}
/// 处理生成完成
class _HandleGenerationCompleteInternal extends SettingGenerationBlocEvent {
const _HandleGenerationCompleteInternal();
}
class _ProcessPendingNodes extends SettingGenerationBlocEvent {
const _ProcessPendingNodes();
}
/// 定时器触发的超时检查内部事件
class _TimeoutCheckInternal extends SettingGenerationBlocEvent {
const _TimeoutCheckInternal();
}