3458 lines
129 KiB
Dart
3458 lines
129 KiB
Dart
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();
|
||
}
|