import 'dart:async'; import 'package:ainoval/services/api_service/repositories/chat_repository.dart'; import 'package:ainoval/services/api_service/repositories/impl/chat_repository_impl.dart'; import 'package:ainoval/services/api_service/repositories/novel_setting_repository.dart'; import 'package:ainoval/services/api_service/repositories/novel_snippet_repository.dart'; import 'package:ainoval/models/novel_setting_item.dart'; import 'package:ainoval/models/novel_snippet.dart'; import 'package:ainoval/models/setting_group.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:uuid/uuid.dart'; import '../../config/app_config.dart'; import '../../models/chat_models.dart'; import '../../models/user_ai_model_config_model.dart'; import '../../services/auth_service.dart'; import '../../utils/logger.dart'; import '../ai_config/ai_config_bloc.dart'; import '../public_models/public_models_bloc.dart'; import 'chat_event.dart'; import 'chat_state.dart'; import '../../models/ai_request_models.dart'; import '../../models/context_selection_models.dart'; class ChatBloc extends Bloc { ChatBloc({ required this.repository, required this.authService, required AiConfigBloc aiConfigBloc, required PublicModelsBloc publicModelsBloc, required this.settingRepository, required this.snippetRepository, }) : _userId = AppConfig.userId ?? '', _aiConfigBloc = aiConfigBloc, _publicModelsBloc = publicModelsBloc, super(ChatInitial()) { _aiConfigSubscription = _aiConfigBloc.stream.listen((aiState) { final currentState = state; if (currentState is ChatSessionActive) { // Find the currently selected model in the new list of configs final newSelectedModel = aiState.configs.firstWhereOrNull( (config) => config.id == currentState.session.selectedModelConfigId, ) ?? aiState.defaultConfig; // Fallback to new default if (newSelectedModel != null && newSelectedModel != currentState.selectedModel) { add(UpdateChatModel( sessionId: currentState.session.id, modelConfigId: newSelectedModel.id, )); } } }); AppLogger.i('ChatBloc', 'Constructor called. Instance hash: ${identityHashCode(this)}'); on(_onLoadChatSessions, transformer: restartable()); on(_onCreateChatSession); on(_onSelectChatSession); on(_onSendMessage); // 🚀 临时移除sequential转换器进行调试 on(_onLoadMoreMessages); on(_onUpdateChatTitle); on(_onExecuteAction); on(_onDeleteChatSession); on(_onCancelRequest); on(_onUpdateChatContext); on(_onUpdateChatModel); on(_onLoadContextData); on(_onCacheSettingsData); on(_onCacheSnippetsData); on(_onUpdateChatConfiguration); } final ChatRepository repository; final AuthService authService; final NovelSettingRepository settingRepository; final NovelSnippetRepository snippetRepository; final String _userId; final AiConfigBloc _aiConfigBloc; final PublicModelsBloc _publicModelsBloc; // 🚀 修改为两层映射:novelId -> sessionId -> config final Map> _sessionConfigs = {}; // 用于跟踪活动的流订阅,以便可以取消它们 // StreamSubscription? _sessionsSubscription; // StreamSubscription? _messagesSubscription; // 用于取消正在进行的消息生成请求 StreamSubscription? _sendMessageSubscription; StreamSubscription? _aiConfigSubscription; // 标记用户是否请求取消,用于在流式处理过程中提前退出 bool _cancelRequested = false; // 临时存储上下文数据,用于在非活动状态时保存加载的数据 List _tempCachedSettings = []; List _tempCachedSettingGroups = []; List _tempCachedSnippets = []; @override Future close() { AppLogger.w('ChatBloc', 'close() method called! Disposing ChatBloc and cancelling subscriptions. Instance hash: ${identityHashCode(this)}'); // _sessionsSubscription?.cancel(); // _messagesSubscription?.cancel(); _sendMessageSubscription?.cancel(); _aiConfigSubscription?.cancel(); return super.close(); } Future _onLoadChatSessions( LoadChatSessions event, Emitter emit) async { AppLogger.i('ChatBloc', '[Event Start] _onLoadChatSessions for novel ${event.novelId}'); emit(ChatSessionsLoading()); final List sessions = []; // 不再需要局部变量 try { // 🚀 传递novelId给repository final stream = repository.fetchUserSessions(_userId, novelId: event.novelId); // 使用 await emit.forEach 处理流 await emit.forEach( stream, onData: (session) { sessions.add(session); // 返回当前状态,直到流结束 emit(ChatSessionsLoading()); return ChatSessionsLoaded(sessions: List.of(sessions)); //return state; // 保持 Loading 状态直到完成 }, onError: (error, stackTrace) { AppLogger.e('ChatBloc', '_onLoadChatSessions stream error', error, stackTrace); // 在 onError 中直接返回错误状态 final errorMessage = '加载会话列表失败: ${ApiExceptionHelper.fromException(error, "加载会话流出错").message}'; return ChatSessionsLoaded(sessions: sessions, error: errorMessage); }, ); AppLogger.i('ChatBloc', '[Stream Complete] _onLoadChatSessions collected ${sessions.length} sessions.'); // 检查 BLoC 是否关闭 if (!isClosed && !emit.isDone) { emit(ChatSessionsLoaded(sessions: sessions)); } else { AppLogger.w('ChatBloc', '[Emit Check] BLoC/Emitter closed before emitting final ChatSessionsLoaded.'); } // ---------- 修改结束 ---------- } catch (e, stackTrace) { AppLogger.e( 'ChatBloc', 'Failed to load chat sessions (stream error or other)', e, stackTrace); // 检查 BLoC 是否关闭 if (!isClosed && !emit.isDone) { final errorMessage = '加载会话列表时发生错误: ${ApiExceptionHelper.fromException(e, "加载会话列表出错").message}'; // 错误发生时,我们没有部分列表,所以 sessions 参数为空 emit(ChatSessionsLoaded( sessions: const [], error: errorMessage)); // 返回空列表和错误 } } finally { // 修改 finally 中的日志级别 AppLogger.i('ChatBloc', '[Event End] _onLoadChatSessions complete.'); // 使用 INFO 级别 } } Future _onCreateChatSession( CreateChatSession event, Emitter emit) async { AppLogger.d('ChatBloc', '[Event Start] _onCreateChatSession'); if (isClosed) { AppLogger.e('ChatBloc', 'Event started but BLoC closed.'); return; } try { final newSession = await repository.createSession( userId: _userId, novelId: event.novelId, metadata: { 'title': event.title, if (event.chapterId != null) 'chapterId': event.chapterId }, ); // 优化:如果当前是列表状态,直接更新;否则重新加载 if (state is ChatSessionsLoaded) { final currentState = state as ChatSessionsLoaded; final updatedSessions = List.from(currentState.sessions) ..add(newSession); // 更新列表,并清除可能存在的错误 emit( currentState.copyWith(sessions: updatedSessions, clearError: true)); AppLogger.d('ChatBloc', '_onCreateChatSession updated existing list.'); // 创建后立即选中 add(SelectChatSession(sessionId: newSession.id, novelId: event.novelId)); } else { // 如果不是列表状态(例如初始状态、错误状态或活动会话状态),触发重新加载 AppLogger.d( 'ChatBloc', '_onCreateChatSession triggering LoadChatSessions.'); add(LoadChatSessions(novelId: event.novelId)); // 在重新加载后,UI 将自然地显示新会话 // 如果需要加载后自动选中,需要在 LoadChatSessions 成功后处理 } AppLogger.d('ChatBloc', '[Event End] _onCreateChatSession successful.'); } catch (e, stackTrace) { AppLogger.e('ChatBloc', '[Event Error] _onCreateChatSession failed.', e, stackTrace); if (!isClosed && !emit.isDone) { final errorMessage = '创建聊天会话失败: ${ApiExceptionHelper.fromException(e, "创建会话出错").message}'; // 尝试在当前状态上显示错误 if (state is ChatSessionsLoaded) { emit((state as ChatSessionsLoaded) .copyWith(error: errorMessage, clearError: false)); } else if (state is ChatSessionActive) { emit((state as ChatSessionActive) .copyWith(error: errorMessage, clearError: false)); } else { emit(ChatError(message: errorMessage)); } } } } Future _onSelectChatSession( SelectChatSession event, Emitter emit) async { AppLogger.d('ChatBloc', '[Event Start] _onSelectChatSession for session ${event.sessionId}'); if (isClosed) { AppLogger.e('ChatBloc', 'Event started but BLoC closed.'); return; } // 取消之前的消息订阅和生成请求 // await _messagesSubscription?.cancel(); // 由 emit.forEach 管理,无需手动取消 await _sendMessageSubscription?.cancel(); _sendMessageSubscription = null; emit(ChatSessionLoading()); AppLogger.d('ChatBloc', '_onSelectChatSession emitted ChatSessionLoading'); try { // 1. 获取会话详情 - 🚀 传递novelId参数 final session = await repository.getSession(_userId, event.sessionId, novelId: event.novelId); // 2. 创建默认上下文 final context = ChatContext( novelId: session.novelId ?? event.novelId ?? '', chapterId: session.metadata?['chapterId'] as String?, relevantItems: const [], ); // 3. 解析选中的模型 UserAIModelConfigModel? selectedModel; final aiState = _aiConfigBloc.state; if (aiState.configs.isNotEmpty) { if (session.selectedModelConfigId != null) { selectedModel = aiState.configs.firstWhereOrNull( (config) => config.id == session.selectedModelConfigId, ); } selectedModel ??= aiState.defaultConfig; } else { AppLogger.w('ChatBloc', '_onSelectChatSession: AiConfigBloc state does not have configs loaded. Will trigger loading.'); // 🚀 如果配置未加载,触发加载 _aiConfigBloc.add(LoadAiConfigs(userId: _userId)); } // 🚀 新增:如果没有可用的私有模型,自动回退到公共模型,避免强制配置私有模型 if (selectedModel == null) { final publicState = _publicModelsBloc.state; if (publicState is PublicModelsLoaded && publicState.models.isNotEmpty) { // 优先选择 gemini-2.0,其次选择包含 gemini/Google 的模型,否则取优先级最高或第一个 var target = publicState.models.firstWhereOrNull( (m) => m.modelId.toLowerCase() == 'gemini-2.0'); if (target == null) { final candidates = publicState.models.where((m) { final p = m.provider.toLowerCase(); final id = m.modelId.toLowerCase(); return p.contains('gemini') || p.contains('google') || id.contains('gemini'); }).toList(); if (candidates.isNotEmpty) { candidates.sort((a, b) => (b.priority ?? 0).compareTo(a.priority ?? 0)); target = candidates.first; } } target ??= publicState.models.first; // 将公共模型映射为临时的用户模型配置,使用 public_ 前缀 if (target != null) { selectedModel = UserAIModelConfigModel.fromJson({ 'id': 'public_${target.id}', 'userId': _userId, 'alias': target.displayName, 'modelName': target.modelId, 'provider': target.provider, 'apiEndpoint': '', 'isDefault': false, 'isValidated': true, 'createdAt': DateTime.now().toIso8601String(), 'updatedAt': DateTime.now().toIso8601String(), }); AppLogger.i('ChatBloc', '未找到私有模型,自动选择公共模型: ${target.displayName} (${target.provider}/${target.modelId})'); } } } // 4. 🚀 获取或创建会话的AI配置 - 使用两层映射 UniversalAIRequest chatConfig; final novelId = session.novelId ?? event.novelId; // 首先检查内存中是否已有配置 if (_sessionConfigs[novelId]?.containsKey(event.sessionId) == true) { chatConfig = _sessionConfigs[novelId]![event.sessionId]!; AppLogger.i('ChatBloc', '使用内存中的会话配置: novelId=$novelId, sessionId=${event.sessionId}'); } else { // 🚀 从Repository缓存中获取配置(已在getSession时缓存) final cachedConfig = ChatRepositoryImpl.getCachedSessionConfig(event.sessionId, novelId: novelId); if (cachedConfig != null) { AppLogger.i('ChatBloc', '从Repository缓存获取会话AI配置成功: novelId=$novelId, sessionId=${event.sessionId}, requestType=${cachedConfig.requestType.value}'); chatConfig = cachedConfig; } else { AppLogger.i('ChatBloc', '缓存中无会话AI配置,创建默认配置: novelId=$novelId, sessionId=${event.sessionId}'); chatConfig = _createDefaultChatConfig(session); } } // 🚀 确保配置中包含当前选择的模型(无论是从缓存获取还是新创建的) if (selectedModel != null) { // 如为公共模型,补充必要的元数据,确保后端走公共模型计费与路由 Map updatedMeta = Map.from(chatConfig.metadata); final String selId = selectedModel.id; if (selId.startsWith('public_')) { final String publicId = selId.substring('public_'.length); updatedMeta['isPublicModel'] = true; updatedMeta['publicModelId'] = publicId; updatedMeta['publicModelConfigId'] = publicId; } else { updatedMeta['isPublicModel'] = false; updatedMeta.remove('publicModelId'); updatedMeta.remove('publicModelConfigId'); } chatConfig = chatConfig.copyWith( modelConfig: selectedModel, metadata: updatedMeta, ); AppLogger.i('ChatBloc', '已将选择的模型设置到会话配置: modelId=${selectedModel.id}, modelName=${selectedModel.modelName}'); } // 将配置存储到两层映射中(无论是从缓存获取还是新创建的) if (novelId != null) { _sessionConfigs[novelId] ??= {}; _sessionConfigs[novelId]![event.sessionId] = chatConfig; AppLogger.i('ChatBloc', '会话配置已存储到内存映射: novelId=$novelId, sessionId=${event.sessionId}'); } // 🚀 添加调试日志,确认配置内容 AppLogger.d('ChatBloc', '配置详情: contextSelections=${chatConfig.contextSelections != null ? "存在(${chatConfig.contextSelections!.availableItems.length}项)" : "不存在"}, requestType=${chatConfig.requestType.value}'); // 6. 发出初始 Activity 状态,标记正在加载历史 emit(ChatSessionActive( session: session, context: context, selectedModel: selectedModel, messages: const [], // 初始空列表 isGenerating: false, isLoadingHistory: true, // 标记正在加载历史 cachedSettings: _tempCachedSettings, // 应用临时保存的设定数据 cachedSettingGroups: _tempCachedSettingGroups, // 应用临时保存的设定组数据 cachedSnippets: _tempCachedSnippets, // 应用临时保存的片段数据 )); AppLogger.d('ChatBloc', '_onSelectChatSession emitted initial ChatSessionActive (loading history)'); // 5. 使用 await emit.forEach 加载消息历史 - 🚀 传递novelId参数 final List messages = []; // 本地列表用于收集消息 final messageStream = repository.getMessageHistory(_userId, event.sessionId, novelId: novelId); AppLogger.d('ChatBloc', '_onSelectChatSession starting message history processing...'); try { // Wrap emit.forEach in try-catch for stream-specific errors await emit.forEach( messageStream, onData: (message) { messages.add(message); // 先收集到本地列表 // 在加载过程中可以不更新 UI,或者只更新 loading 状态 return state; // 保持当前状态或 Loading 状态 }, onError: (error, stackTrace) { AppLogger.e('ChatBloc', 'Error loading message history stream', error, stackTrace); final currentState = state; final errorMessage = '加载消息历史失败: ${_formatApiError(error, "加载历史出错")}'; if (currentState is ChatSessionActive && currentState.session.id == event.sessionId) { if (!isClosed && !emit.isDone) { return currentState.copyWith( isLoadingHistory: false, error: errorMessage, clearError: false, ); } } if (!isClosed && !emit.isDone) { return ChatError(message: errorMessage); } return state; }, ); // ---- emit.forEach 成功完成 ---- AppLogger.i('ChatBloc', '[Callback] _onSelectChatSession message history stream onDone. Collected ${messages.length} messages.'); // ----------- 添加排序逻辑 ----------- messages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); AppLogger.d('ChatBloc', 'Messages sorted by timestamp ASC.'); // --------------------------------- // 再次检查 BLoC 和 emitter 状态,并确认当前会话仍然是目标会话 final finalState = state; if (!isClosed && !emit.isDone && finalState is ChatSessionActive && finalState.session.id == event.sessionId) { emit(finalState.copyWith( messages: messages, // <--- 使用排序后的列表 isLoadingHistory: false, // 标记历史加载完成 clearError: true, )); AppLogger.d('ChatBloc', '[History onDone Check] PASSED. Emitted final sorted history.'); } else { AppLogger.w('ChatBloc', '[History onDone Check] State changed, BLoC/Emitter closed, or state type mismatch. Ignoring emit.'); } } catch (e, stackTrace) { // Catch potential errors from the stream itself or sorting AppLogger.e( 'ChatBloc', 'Error during message history processing or sorting', e, stackTrace); if (!isClosed && !emit.isDone) { final errorMessage = '处理消息历史时出错: ${_formatApiError(e, "处理历史出错")}'; final currentState = state; if (currentState is ChatSessionActive && currentState.session.id == event.sessionId) { emit(currentState.copyWith( isLoadingHistory: false, error: errorMessage, clearError: false)); } else { emit(ChatError(message: errorMessage)); } } } } catch (e, stackTrace) { AppLogger.e( 'ChatBloc', '[Event Error] _onSelectChatSession (initial get failed).', e, stackTrace); if (!isClosed && !emit.isDone) { final errorMessage = '加载会话失败: ${_formatApiError(e, "加载会话信息出错")}'; emit(ChatError(message: errorMessage)); } } AppLogger.d( 'ChatBloc', '[Event End Setup] _onSelectChatSession setup complete.'); } Future _onSendMessage( SendMessage event, Emitter emit) async { AppLogger.i('ChatBloc', '🚀🚀🚀 收到发送消息事件: ${event.content}, BLoC实例: ${identityHashCode(this)}, isClosed: $isClosed'); // 新的发送开始前清除任何残留的取消标志 _cancelRequested = false; if (state is ChatSessionActive) { final currentState = state as ChatSessionActive; // 🚀 添加状态检查,确保不在生成中才能发送新消息 if (currentState.isGenerating) { AppLogger.w('ChatBloc', '正在生成中,忽略新消息发送请求'); return; } AppLogger.i('ChatBloc', '开始发送消息到会话: ${currentState.session.id}'); // 🚀 检查是否是第一条消息,如果是则立即更新前端标题 final isFirstMessage = currentState.messages.where((msg) => msg.role == MessageRole.user).isEmpty; if (isFirstMessage) { String newTitle; if (event.content.length > 10) { // 取前10个字符作为标题 newTitle = event.content.substring(0, 10); // 如果截断处不是完整字符,找到最后一个空格位置 int lastSpace = newTitle.lastIndexOf(' '); if (lastSpace > 5) { // 确保至少有5个字符 newTitle = newTitle.substring(0, lastSpace); } newTitle = newTitle + "..."; } else { newTitle = event.content; } // 移除换行符和多余的空格 newTitle = newTitle.replaceAll(RegExp(r'\s+'), ' ').trim(); // 如果标题为空,使用默认格式 if (newTitle.isEmpty) { newTitle = "聊天会话 ${DateTime.now().toString().substring(5, 16)}"; } AppLogger.i('ChatBloc', '第一条消息,立即更新前端标题: $newTitle'); // 立即更新前端会话标题(不等待后端响应) final updatedSession = currentState.session.copyWith( title: newTitle, lastUpdatedAt: DateTime.now(), ); // 先更新状态以显示新标题 emit(currentState.copyWith(session: updatedSession)); } // 🚀 检查并确保会话配置存在 final novelId = currentState.session.novelId; if (novelId != null && _sessionConfigs[novelId]?.containsKey(currentState.session.id) != true) { AppLogger.w('ChatBloc', '会话配置不存在,创建默认配置: novelId=$novelId, sessionId=${currentState.session.id}'); final defaultConfig = _createDefaultChatConfig(currentState.session); if (currentState.selectedModel != null) { _sessionConfigs[novelId] ??= {}; _sessionConfigs[novelId]![currentState.session.id] = defaultConfig.copyWith(modelConfig: currentState.selectedModel); } else { _sessionConfigs[novelId] ??= {}; _sessionConfigs[novelId]![currentState.session.id] = defaultConfig; } AppLogger.i('ChatBloc', '已为会话创建默认配置: novelId=$novelId, sessionId=${currentState.session.id}'); } final userMessage = ChatMessage( sender: MessageSender.user, id: const Uuid().v4(), sessionId: currentState.session.id, role: MessageRole.user, content: event.content, timestamp: DateTime.now(), status: MessageStatus.sent, ); ChatMessage? placeholderMessage; try { placeholderMessage = ChatMessage( sender: MessageSender.ai, id: const Uuid().v4(), sessionId: currentState.session.id, role: MessageRole.assistant, content: '', timestamp: DateTime.now(), status: MessageStatus.pending, ); AppLogger.i('ChatBloc', '创建占位符消息: ${placeholderMessage.id}'); // 在发起请求前,先更新UI,添加用户消息和占位符 emit(currentState.copyWith( messages: [...currentState.messages, userMessage, placeholderMessage], isGenerating: true, error: null, // 清除之前的错误(如果有) )); AppLogger.i('ChatBloc', '准备调用_handleStreamedResponse'); // 🚀 使用当前的聊天配置发起流式请求 UniversalAIRequest? chatConfig; if (novelId != null) { chatConfig = _sessionConfigs[novelId]?[currentState.session.id]; } await _handleStreamedResponse( emit, placeholderMessage.id, event.content, chatConfig); } catch (e, stackTrace) { AppLogger.e('ChatBloc', '发送消息失败 (在调用 _handleStreamedResponse 之前或期间出错)', e, stackTrace); // 确保在错误发生时也能更新状态 if (state is ChatSessionActive) { final errorState = state as ChatSessionActive; final errorMessages = List.from(errorState.messages); // 如果 placeholder 存在于列表中,标记为错误 if (placeholderMessage != null) { final errorIndex = errorMessages .indexWhere((msg) => msg.id == placeholderMessage!.id); if (errorIndex != -1) { errorMessages[errorIndex] = errorMessages[errorIndex].copyWith( content: '生成回复时出错: ${ApiExceptionHelper.fromException(e, "发送消息失败").message}', // 使用辅助方法 status: MessageStatus.error, ); emit(errorState.copyWith( messages: errorMessages, isGenerating: false, // 即使出错也要停止生成状态 error: ApiExceptionHelper.fromException(e, '发送消息失败') .message, // 使用辅助方法 )); } else { // 如果 placeholder 不在列表里(理论上不应该发生,除非状态更新逻辑有问题) AppLogger.w( 'ChatBloc', '未找到ID为 ${placeholderMessage.id} 的占位符消息标记错误'); emit(errorState.copyWith( isGenerating: false, )); } } else { // 如果 placeholder 尚未创建就出错 emit(errorState.copyWith( isGenerating: false, error: ApiExceptionHelper.fromException(e, '发送消息失败') .message, // 使用辅助方法 )); } } } } else { // 🚀 添加明确的日志,说明为什么消息发送被忽略 AppLogger.w('ChatBloc', '发送消息被忽略,当前状态不是ChatSessionActive: ${state.runtimeType}'); if (state is ChatSessionsLoaded) { AppLogger.i('ChatBloc', '当前在会话列表状态,需要先选择一个会话'); } else if (state is ChatSessionLoading) { AppLogger.i('ChatBloc', '会话正在加载中,请等待加载完成'); } else if (state is ChatError) { AppLogger.i('ChatBloc', '当前处于错误状态,无法发送消息'); } } } Future _onLoadMoreMessages( LoadMoreMessages event, Emitter emit) async { // TODO: 实现加载更多历史消息的逻辑 // 需要修改 repository.getMessageHistory 以支持分页或 "before" 参数 // 然后将获取到的旧消息插入到当前消息列表的前面 AppLogger.w('ChatBloc', '_onLoadMoreMessages 尚未实现'); } Future _onUpdateChatTitle( UpdateChatTitle event, Emitter emit) async { if (state is ChatSessionActive) { final currentState = state as ChatSessionActive; try { final updatedSession = await repository.updateSession( userId: _userId, sessionId: currentState.session.id, updates: {'title': event.newTitle}, novelId: currentState.session.novelId, ); emit(currentState.copyWith( session: updatedSession, )); } catch (e) { emit(currentState.copyWith( error: '更新标题失败: ${e.toString()}', )); } } } Future _onExecuteAction( ExecuteAction event, Emitter emit) async { if (state is ChatSessionActive) { final currentState = state as ChatSessionActive; try { // 根据操作类型执行不同的动作 switch (event.action.type) { case ActionType.applyToEditor: // 应用到编辑器的逻辑 // 这部分需要与编辑器模块交互,在第二周迭代中可以先简单实现 break; case ActionType.createCharacter: // 创建角色的逻辑 break; case ActionType.createLocation: // 创建地点的逻辑 break; case ActionType.generatePlot: // 生成情节的逻辑 break; case ActionType.expandScene: // 扩展场景的逻辑 break; case ActionType.createChapter: // 创建章节的逻辑 break; case ActionType.analyzeSentiment: // 分析情感的逻辑 break; case ActionType.fixGrammar: // 修复语法的逻辑 break; } } catch (e) { emit(currentState.copyWith( error: '执行操作失败: ${e.toString()}', )); } } } Future _onDeleteChatSession( DeleteChatSession event, Emitter emit) async { List? previousSessions; if (state is ChatSessionsLoaded) { previousSessions = (state as ChatSessionsLoaded).sessions; } else if (state is ChatSessionActive) { // 如果从活动会话删除,我们可能没有完整的列表状态,但可以尝试保留 // 这里简化处理,不保留列表 } try { // 🚀 获取会话的novelId来删除配置缓存 String? novelId; if (state is ChatSessionActive) { final currentState = state as ChatSessionActive; if (currentState.session.id == event.sessionId) { novelId = currentState.session.novelId; } } await repository.deleteSession(_userId, event.sessionId, novelId: novelId); // 清除本地配置缓存 if (novelId != null) { _sessionConfigs[novelId]?.remove(event.sessionId); if (_sessionConfigs[novelId]?.isEmpty == true) { _sessionConfigs.remove(novelId); } } // 从状态中移除会话 if (previousSessions != null) { final updatedSessions = previousSessions .where((session) => session.id != event.sessionId) .toList(); emit(ChatSessionsLoaded(sessions: updatedSessions)); } else { // 如果之前不是列表状态,或当前活动会话被删除,回到初始状态 // 让UI决定是否需要重新加载列表 emit(ChatInitial()); } } catch (e, stackTrace) { // 添加 stackTrace AppLogger.e('ChatBloc', '删除会话失败', e, stackTrace); // 无法在 ChatSessionsLoaded 添加错误,改为发出 ChatError // 保留之前的状态可能导致UI不一致 final errorMessage = '删除会话失败: ${ApiExceptionHelper.fromException(e, "删除会话出错").message}'; // 尝试在当前状态显示错误,如果不行就发 ChatError if (state is ChatSessionsLoaded) { // 现在可以使用 copyWith 来在 ChatSessionsLoaded 状态下显示错误 final currentState = state as ChatSessionsLoaded; // 在保留现有列表的同时添加错误消息 emit(currentState.copyWith(error: errorMessage)); } else if (state is ChatSessionActive) { emit((state as ChatSessionActive).copyWith(error: errorMessage)); } else { // 如果是其他状态,发出全局错误 emit(ChatError(message: errorMessage)); } } } Future _onCancelRequest( CancelOngoingRequest event, Emitter emit) async { AppLogger.w('ChatBloc', '收到取消请求,开始清理资源'); // 取消正在进行的流式订阅 await _sendMessageSubscription?.cancel(); _sendMessageSubscription = null; // 设置取消标志,供 _handleStreamedResponse 检测 _cancelRequested = true; // 确保无论当前状态如何都重置isGenerating if (state is ChatSessionActive) { final currentState = state as ChatSessionActive; AppLogger.w('ChatBloc', '取消请求 - 更新UI状态,确保停止生成状态'); final latestMessages = List.from(currentState.messages); final lastPendingIndex = latestMessages.lastIndexWhere((msg) => msg.role == MessageRole.assistant && (msg.status == MessageStatus.pending || msg.status == MessageStatus.streaming) // 包含 streaming 状态 ); if (lastPendingIndex != -1) { latestMessages[lastPendingIndex] = latestMessages[lastPendingIndex] .copyWith( // 保留已生成的内容,不再追加“已取消”标签 status: MessageStatus.sent, // 将状态从 streaming/pending 置为 sent,表示已结束 ); } else { // 未找到仍在生成的消息,可能已经结束 AppLogger.w('ChatBloc', '未找到待取消的streaming/pending消息,可能已结束'); } // 🚀 关键修复:无论是否有正在进行的生成,都确保isGenerating设为false,清除错误状态 emit(currentState.copyWith( messages: latestMessages, isGenerating: false, error: null, clearError: true, )); AppLogger.i('ChatBloc', '取消完成,isGenerating已设为false,应该可以发送新消息'); } else { AppLogger.w('ChatBloc', '取消请求时状态不是ChatSessionActive: ${state.runtimeType}'); } } Future _onUpdateChatContext( UpdateChatContext event, Emitter emit) async { if (state is ChatSessionActive) { final currentState = state as ChatSessionActive; emit(currentState.copyWith( context: event.context, )); } } // 修改:处理流式响应的辅助方法,接收 placeholderId 和 chatConfig // 使用 await emit.forEach 重构 Future _handleStreamedResponse( Emitter emit, String placeholderId, String userContent, UniversalAIRequest? chatConfig) async { AppLogger.i('ChatBloc', '_handleStreamedResponse开始执行,placeholderId: $placeholderId'); // --- Initial state check --- if (state is! ChatSessionActive) { AppLogger.e('ChatBloc', '_handleStreamedResponse called while not in ChatSessionActive state'); // Cannot proceed without active state, emit error if possible // Emitter might be closed here already if called incorrectly, so check if (!emit.isDone) { try { emit(const ChatError(message: '内部错误: 无法在非活动会话中处理流')); } catch (e) { AppLogger.e('ChatBloc', 'Failed to emit error state', e); } } return; } // Capture initial state specifics final initialState = state as ChatSessionActive; final currentSessionId = initialState.session.id; const initialRole = MessageRole.assistant; AppLogger.i('ChatBloc', '当前会话ID: $currentSessionId, 用户消息: $userContent'); if (_cancelRequested) { _cancelRequested = false; AppLogger.w('ChatBloc', '_handleStreamedResponse detected residual cancel flag, aborting'); if (!emit.isDone && state is ChatSessionActive) { emit((state as ChatSessionActive).copyWith(isGenerating: false)); } return; } StringBuffer contentBuffer = StringBuffer(); try { // 🚀 构建用于发送的配置,将用户消息内容填充到 prompt 字段 UniversalAIRequest? configToSend; if (chatConfig != null) { configToSend = chatConfig.copyWith( prompt: userContent, // 将当前用户输入填充到prompt字段 modelConfig: initialState.selectedModel, // 使用当前选中的模型 ); AppLogger.i('ChatBloc', '使用聊天配置: ${configToSend.requestType.value}'); } else { AppLogger.i('ChatBloc', '没有聊天配置,使用默认设置'); } AppLogger.i('ChatBloc', '开始调用repository.streamMessage'); final stream = repository.streamMessage( userId: _userId, sessionId: currentSessionId, content: userContent, config: configToSend, // 🚀 传递完整的配置 novelId: initialState.session.novelId, // 🚀 修复:添加缺失的novelId参数 // Pass configId if needed: // configId: initialState.selectedModel?.id, ); AppLogger.i('ChatBloc', 'streamMessage调用完成,开始监听流数据'); // --- Use await emit.forEach --- await emit.forEach( stream, onData: (chunk) { // --- Per-chunk state validation --- // Get the absolute latest state *inside* onData final currentState = state; // Check if state is still valid *for this operation* if (currentState is! ChatSessionActive || currentState.session.id != currentSessionId) { AppLogger.w('ChatBloc', 'emit.forEach onData: State changed during stream processing. Stopping.'); // Throwing an error here will exit emit.forEach and go to the outer catch block throw StateError('Chat session changed during streaming'); } // --- State is valid, proceed --- // 如果途中收到取消请求,则忽略后续 chunk,不再更新 UI if (_cancelRequested) { return currentState; // 不做任何修改,维持现状 } // 🚀 如果收到的是完整消息(DELIVERED状态),直接处理为最终消息 if (chunk.status == MessageStatus.sent || chunk.status == MessageStatus.delivered) { AppLogger.i('ChatBloc', '收到完整消息,直接设置为最终状态: messageId=${chunk.id}, status=${chunk.status}'); final latestMessages = List.from(currentState.messages); final aiMessageIndex = latestMessages.indexWhere((msg) => msg.id == placeholderId); if (aiMessageIndex != -1) { final finalMessage = ChatMessage( sender: MessageSender.ai, id: placeholderId, // Keep placeholder ID role: initialRole, content: chunk.content, // Use complete content from backend timestamp: chunk.timestamp ?? DateTime.now(), status: MessageStatus.sent, // Final status sessionId: currentSessionId, userId: _userId, novelId: currentState.session.novelId, metadata: chunk.metadata ?? latestMessages[aiMessageIndex].metadata, actions: chunk.actions ?? latestMessages[aiMessageIndex].actions, ); latestMessages[aiMessageIndex] = finalMessage; // 🚀 第一条消息的标题已在前端立即更新,无需检查后端标题 ChatSession updatedSession = currentState.session; // 🚀 对于完整消息,设置isGenerating为false return currentState.copyWith( messages: latestMessages, session: updatedSession, isGenerating: false, // Generation complete clearError: true, ); } else { AppLogger.w('ChatBloc', '_handleStreamedResponse: 未找到ID为 $placeholderId 的占位符进行最终更新'); throw StateError('Placeholder message lost during streaming'); } } else { // 🚀 处理流式块 - 累积内容并更新UI以触发打字机效果 contentBuffer.write(chunk.content); //AppLogger.v('ChatBloc', '累积流式内容: ${chunk.content}, 当前总长度: ${contentBuffer.length}'); final latestMessages = List.from(currentState.messages); final aiMessageIndex = latestMessages.indexWhere((msg) => msg.id == placeholderId); if (aiMessageIndex != -1) { final updatedStreamingMessage = ChatMessage( sender: MessageSender.ai, id: placeholderId, // Keep placeholder ID role: initialRole, content: contentBuffer.toString(), // 🚀 使用累积的内容 timestamp: DateTime.now(), status: MessageStatus.streaming, // 🚀 保持streaming状态以触发打字机效果 sessionId: currentSessionId, userId: _userId, novelId: currentState.session.novelId, metadata: chunk.metadata ?? latestMessages[aiMessageIndex].metadata, actions: chunk.actions ?? latestMessages[aiMessageIndex].actions, ); latestMessages[aiMessageIndex] = updatedStreamingMessage; // Return the *new state* to be emitted by forEach return currentState.copyWith( messages: latestMessages, isGenerating: true, // Still generating ); } else { AppLogger.w('ChatBloc', '_handleStreamedResponse: 未找到ID为 $placeholderId 的占位符进行流式更新'); // Cannot continue if placeholder lost, throw error to exit throw StateError('Placeholder message lost during streaming'); } } }, onError: (error, stackTrace) { // This onError is for the *stream itself* having an error AppLogger.e( 'ChatBloc', 'Stream error in emit.forEach', error, stackTrace); final currentState = state; // Get state at the time of error // 忽略用户主动取消抛出的 CancelledByUser 错误 if (error is StateError && error.message == 'CancelledByUser') { AppLogger.i('ChatBloc', '流被用户取消,忽略错误处理'); return state; } final errorMessage = ApiExceptionHelper.fromException(error, '流处理失败').message; if (currentState is ChatSessionActive && currentState.session.id == currentSessionId) { // Return the error state to be emitted by forEach return currentState.copyWith( messages: _markPlaceholderAsError(currentState.messages, placeholderId, contentBuffer.toString(), errorMessage), isGenerating: false, error: errorMessage, clearError: false, ); } // If state changed before stream error, return a generic error state return ChatError(message: errorMessage); }, ); // ---- Stream finished successfully (await emit.forEach completed without error) ---- // Get final state AFTER the loop finishes final finalState = state; if (finalState is ChatSessionActive && finalState.session.id == currentSessionId) { final latestMessages = List.from(finalState.messages); final aiMessageIndex = latestMessages.indexWhere((msg) => msg.id == placeholderId); if (aiMessageIndex != -1) { final finalMessage = ChatMessage( sender: MessageSender.ai, id: placeholderId, // Keep placeholder ID role: initialRole, content: contentBuffer.toString(), // Final content timestamp: DateTime.now(), // Final timestamp status: MessageStatus.sent, // Final status: sent sessionId: currentSessionId, userId: _userId, novelId: finalState.session.novelId, // Use latest known metadata/actions before finalizing metadata: latestMessages[aiMessageIndex].metadata, actions: latestMessages[aiMessageIndex].actions, ); latestMessages[aiMessageIndex] = finalMessage; // 🚀 第一条消息的标题已在前端立即更新,无需再次检查后端标题 // Emit the final state explicitly after the loop emit(finalState.copyWith( messages: latestMessages, isGenerating: false, // Generation complete clearError: true, // Clear any previous non-fatal errors shown during streaming )); } else { AppLogger.w('ChatBloc', '_handleStreamedResponse (onDone): 未找到ID为 $placeholderId 进行最终更新'); if (finalState.isGenerating) { emit(finalState.copyWith( isGenerating: false)); // Ensure generating stops } } } else { AppLogger.w('ChatBloc', 'Stream completed, but state changed or invalid. Final update skipped.'); // If the state changed BUT we were generating, make sure to stop it if (state is ChatSessionActive && (state as ChatSessionActive).isGenerating) { emit((state as ChatSessionActive).copyWith(isGenerating: false)); } else if (state is! ChatSessionActive) { // This case is tricky, maybe emit ChatError or just log AppLogger.e('ChatBloc', 'Stream completed, state is not Active, but maybe was generating? State: ${state.runtimeType}'); } } } catch (error, stackTrace) { // Catches errors from: // - Initial repository.streamMessage call // - Errors re-thrown from the stream's `onError` that emit.forEach catches // - The StateErrors thrown in `onData` if state changes or placeholder is lost AppLogger.e( 'ChatBloc', 'Error during _handleStreamedResponse processing loop', error, stackTrace); // Check emitter status *before* attempting to emit if (!emit.isDone) { final currentState = state; // Get state at the time of catch final errorMessage = (error is StateError) ? '内部错误: ${error.message}' // Keep StateError messages distinct : ApiExceptionHelper.fromException(error, '处理流响应失败').message; if (currentState is ChatSessionActive && currentState.session.id == currentSessionId) { // Attempt to emit the error state for the correct session emit(currentState.copyWith( messages: _markPlaceholderAsError(currentState.messages, placeholderId, contentBuffer.toString(), errorMessage), isGenerating: false, // Stop generation on error error: errorMessage, clearError: false, )); } else { // If state changed before catch, emit generic error AppLogger.w('ChatBloc', 'Caught error, but state changed. Emitting generic ChatError.'); emit(ChatError(message: errorMessage)); } } else { AppLogger.w('ChatBloc', 'Caught error, but emitter is done. Cannot emit error state.'); } } finally { // No explicit subscription cleanup needed with emit.forEach AppLogger.d('ChatBloc', '_handleStreamedResponse finished processing for placeholder $placeholderId'); // Ensure `isGenerating` is false if the process ends unexpectedly without explicit state update // This is a safety net. if (state is ChatSessionActive && (state as ChatSessionActive).isGenerating && (state as ChatSessionActive).session.id == currentSessionId) { AppLogger.w('ChatBloc', '_handleStreamedResponse finally: State still shows isGenerating. Forcing to false.'); if (!emit.isDone) { emit((state as ChatSessionActive).copyWith(isGenerating: false)); } } // 流处理结束后重置取消标志 _cancelRequested = false; } } // 辅助方法: 将占位符消息标记为错误 (确保使用 MessageStatus.error) List _markPlaceholderAsError(List messages, String placeholderId, String bufferedContent, String errorMessage) { final listCopy = List.from(messages); final errorIndex = listCopy.indexWhere((msg) => msg.id == placeholderId); if (errorIndex != -1) { final existingMessage = listCopy[errorIndex]; listCopy[errorIndex] = existingMessage.copyWith( content: bufferedContent.isNotEmpty ? '$bufferedContent\n\n[错误: $errorMessage]' : '[错误: $errorMessage]', status: MessageStatus.error, // Mark as error timestamp: DateTime.now(), // Update timestamp ); } else { AppLogger.w('ChatBloc', '_markPlaceholderAsError: 未找到ID为 $placeholderId 的占位符标记错误'); } return listCopy; } Future _onUpdateChatModel( UpdateChatModel event, Emitter emit) async { final currentState = state; if (currentState is ChatSessionActive && currentState.session.id == event.sessionId) { UserAIModelConfigModel? newSelectedModel; final aiState = _aiConfigBloc.state; // 1. 先在 AiConfigBloc 中查找私有模型 if (aiState.configs.isNotEmpty) { newSelectedModel = aiState.configs.firstWhereOrNull( (config) => config.id == event.modelConfigId, ); } // 2. 如果在私有模型中没找到,检查是否是公共模型 if (newSelectedModel == null) { // 🚀 尝试从PublicModelsBloc中查找公共模型 final publicState = _publicModelsBloc.state; if (publicState is PublicModelsLoaded) { // 检查是否是public_前缀的ID(临时配置ID)或直接的公共模型ID String targetPublicModelId = event.modelConfigId; if (targetPublicModelId.startsWith('public_')) { targetPublicModelId = targetPublicModelId.substring('public_'.length); } final publicModel = publicState.models.firstWhereOrNull( (model) => model.id == targetPublicModelId, ); if (publicModel != null) { // 🚀 为公共模型创建临时的UserAIModelConfigModel newSelectedModel = UserAIModelConfigModel.fromJson({ 'id': 'public_${publicModel.id}', // 使用前缀标识公共模型 'userId': _userId, 'alias': publicModel.displayName, 'modelName': publicModel.modelId, 'provider': publicModel.provider, 'apiEndpoint': '', // 公共模型没有单独的apiEndpoint 'isDefault': false, 'isValidated': true, 'createdAt': DateTime.now().toIso8601String(), 'updatedAt': DateTime.now().toIso8601String(), }); AppLogger.i('ChatBloc', '_onUpdateChatModel: 找到公共模型并创建临时配置 - publicModelId: ${publicModel.id}, displayName: ${publicModel.displayName}'); } } } if (newSelectedModel == null) { // 添加日志记录找不到模型的具体ID AppLogger.w('ChatBloc', '_onUpdateChatModel: Model config with ID ${event.modelConfigId} not found in both AiConfigBloc and PublicModelsBloc.'); // --- 添加这行日志来查看当前状态 --- AppLogger.d('ChatBloc', 'Current AiConfigState: Status=${aiState.status}, Config IDs=[${aiState.configs.map((c) => c.id).join(', ')}], DefaultConfig ID=${aiState.defaultConfig?.id}'); final publicState = _publicModelsBloc.state; if (publicState is PublicModelsLoaded) { AppLogger.d('ChatBloc', 'Current PublicModelsState: Public Model IDs=[${publicState.models.map((m) => m.id).join(', ')}]'); } else { AppLogger.d('ChatBloc', 'PublicModelsState: ${publicState.runtimeType}'); } // -------------------------------------------------- emit(currentState.copyWith(error: '选择的模型配置未找到或未加载', clearError: false)); return; } try { // 2. Update the backend session await repository.updateSession( userId: _userId, sessionId: event.sessionId, updates: {'selectedModelConfigId': event.modelConfigId}, novelId: currentState.session.novelId); // 3. Update the session object in the state final updatedSession = currentState.session.copyWith( selectedModelConfigId: event.modelConfigId, lastUpdatedAt: DateTime.now(), ); // 4. 🚀 更新会话配置中的模型信息 final novelId = currentState.session.novelId; if (novelId != null) { final currentConfig = _sessionConfigs[novelId]?[event.sessionId]; if (currentConfig != null) { final updatedConfig = currentConfig.copyWith(modelConfig: newSelectedModel); _sessionConfigs[novelId] ??= {}; _sessionConfigs[novelId]![event.sessionId] = updatedConfig; AppLogger.i('ChatBloc', '已更新会话配置中的模型: novelId=$novelId, sessionId=${event.sessionId}, modelId=${newSelectedModel.id}'); } } // 5. Emit the new state with updated session and selectedModel emit(currentState.copyWith( session: updatedSession, selectedModel: newSelectedModel, clearError: true, configUpdateTimestamp: DateTime.now(), // 🚀 触发UI重建 )); AppLogger.i('ChatBloc', '_onUpdateChatModel successful for session ${event.sessionId}, new model ${event.modelConfigId}'); } catch (e, stackTrace) { AppLogger.e('ChatBloc', '_onUpdateChatModel failed to update repository', e, stackTrace); emit(currentState.copyWith( error: '更新模型失败: ${_formatApiError(e, "更新模型失败")}', clearError: false, )); } } else { AppLogger.w('ChatBloc', '_onUpdateChatModel called with non-matching state or session ID.'); } } // 添加一个辅助方法来格式化错误(如果 ApiExceptionHelper 不可用) String _formatApiError(Object error, [String defaultPrefix = '操作失败']) { return '$defaultPrefix: ${error.toString()}'; } /// 加载上下文数据(设定和片段) Future _onLoadContextData( LoadContextData event, Emitter emit ) async { try { AppLogger.i('ChatBloc', '开始加载上下文数据,当前状态: ${state.runtimeType}'); // 并行加载设定和片段数据 final futures = await Future.wait([ _loadSettingsData(event.novelId), _loadSnippetsData(event.novelId), ]); final settingsData = futures[0] as Map; final snippetsData = futures[1] as List; AppLogger.i('ChatBloc', '上下文数据加载完成: ${settingsData['settings'].length} 设定, ${settingsData['groups'].length} 组, ${snippetsData.length} 片段'); // 如果当前状态是ChatSessionActive,更新缓存数据 final currentState = state; if (currentState is ChatSessionActive) { emit(currentState.copyWith( cachedSettings: settingsData['settings'], cachedSettingGroups: settingsData['groups'], cachedSnippets: snippetsData, isLoadingContextData: false, )); } else { // 如果不是活动状态,将数据保存到临时变量中 _tempCachedSettings = settingsData['settings']; _tempCachedSettingGroups = settingsData['groups']; _tempCachedSnippets = snippetsData; AppLogger.i('ChatBloc', '当前状态非ChatSessionActive,上下文数据已保存到临时变量'); } } catch (e, stackTrace) { AppLogger.e('ChatBloc', '加载上下文数据失败', e, stackTrace); final currentState = state; if (currentState is ChatSessionActive) { emit(currentState.copyWith( isLoadingContextData: false, error: '加载上下文数据失败: ${e.toString()}', )); } } } /// 缓存设定数据 Future _onCacheSettingsData( CacheSettingsData event, Emitter emit, ) async { final currentState = state; if (currentState is ChatSessionActive) { emit(currentState.copyWith( cachedSettings: event.settings, cachedSettingGroups: event.settingGroups, )); } } /// 缓存片段数据 Future _onCacheSnippetsData( CacheSnippetsData event, Emitter emit, ) async { final currentState = state; if (currentState is ChatSessionActive) { emit(currentState.copyWith( cachedSnippets: event.snippets, )); } } /// 加载设定数据 Future> _loadSettingsData(String novelId) async { try { final futures = await Future.wait([ settingRepository.getNovelSettingItems( novelId: novelId, page: 0, size: 100, // 限制数量避免过多数据 sortBy: 'createdAt', sortDirection: 'desc', ), settingRepository.getNovelSettingGroups(novelId: novelId), ]); return { 'settings': futures[0] as List, 'groups': futures[1] as List, }; } catch (e) { AppLogger.e('ChatBloc', '加载设定数据失败', e); return { 'settings': [], 'groups': [], }; } } /// 加载片段数据 Future> _loadSnippetsData(String novelId) async { try { final result = await snippetRepository.getSnippetsByNovelId( novelId, page: 0, size: 50, // 限制数量避免过多数据 ); return result.content; } catch (e) { AppLogger.e('ChatBloc', '加载片段数据失败', e); return []; } } /// 🚀 更新聊天配置 Future _onUpdateChatConfiguration( UpdateChatConfiguration event, Emitter emit) async { AppLogger.d('ChatBloc', '[Event Start] _onUpdateChatConfiguration for session ${event.sessionId}'); final currentState = state; if (currentState is ChatSessionActive && currentState.session.id == event.sessionId) { try { // 🚀 更新内存映射中的配置 final novelId = currentState.session.novelId ?? event.config.novelId; if (novelId != null) { _sessionConfigs[novelId] ??= {}; _sessionConfigs[novelId]![event.sessionId] = event.config; // 🚀 同时更新Repository缓存中的配置 ChatRepositoryImpl.cacheSessionConfig(event.sessionId, event.config, novelId: novelId); // 配置已更新到内存映射,发出状态变更通知UI重建 emit(currentState.copyWith( clearError: true, configUpdateTimestamp: DateTime.now(), // 🚀 添加时间戳确保状态变化 )); AppLogger.i('ChatBloc', '_onUpdateChatConfiguration successful for session ${event.sessionId}'); AppLogger.d('ChatBloc', 'Updated config - Instructions: ${event.config.instructions?.isNotEmpty == true ? "有" : "无"}, ' 'Context selections: ${event.config.contextSelections?.selectedCount ?? 0}, ' 'Smart context: ${event.config.enableSmartContext}'); } else { AppLogger.w('ChatBloc', '无法更新配置:缺少novelId信息'); emit(currentState.copyWith( error: '更新聊天配置失败: 缺少小说ID信息', clearError: false, )); } } catch (e, stackTrace) { AppLogger.e('ChatBloc', '_onUpdateChatConfiguration failed', e, stackTrace); emit(currentState.copyWith( error: '更新聊天配置失败: ${_formatApiError(e, "更新配置失败")}', clearError: false, )); } } else { AppLogger.w('ChatBloc', '_onUpdateChatConfiguration called with non-matching state or session ID. ' 'Current state: ${currentState.runtimeType}, ' 'Current session: ${currentState is ChatSessionActive ? currentState.session.id : "N/A"}, ' 'Target session: ${event.sessionId}'); } } /// 🚀 获取会话配置(添加novelId校验) UniversalAIRequest? getSessionConfig(String sessionId, String novelId) { final config = _sessionConfigs[novelId]?[sessionId]; // 🚀 新增:检查配置是否属于当前小说 if (config != null && config.novelId != null && config.novelId != novelId) { AppLogger.w('ChatBloc', '🚨 getSessionConfig($sessionId): 配置存在但不属于当前小说(配置小说ID: ${config.novelId}, 请求小说ID: $novelId)'); return null; } AppLogger.d('ChatBloc', '🔍 getSessionConfig($sessionId, $novelId): 配置${config != null ? "存在且匹配" : "不存在"}, contextSelections=${config?.contextSelections != null ? "存在(可用${config!.contextSelections!.availableItems.length}项,已选${config.contextSelections!.selectedCount}项)" : "不存在"}'); return config; } /// 🚀 构建上下文选择数据 ContextSelectionData? _buildContextSelectionData(ChatSession session) { if (session.novelId == null) return null; // 从EditorBloc获取Novel数据 final editorState = _aiConfigBloc.state; // 这里需要访问EditorBloc,但我们没有直接引用 // 暂时先不创建,让UI层根据state中的缓存数据来构建。 // 这样可以避免一个空的ContextSelectionData覆盖掉由UI异步构建的真实数据。 return null; /* return ContextSelectionData( novelId: session.novelId, availableItems: [], flatItems: {}, ); */ } /// 🚀 创建默认的聊天配置 UniversalAIRequest _createDefaultChatConfig(ChatSession session) { // 构建上下文选择数据 final contextSelectionData = _buildContextSelectionData(session); return UniversalAIRequest( requestType: AIRequestType.chat, userId: _userId, sessionId: session.id, novelId: session.novelId, modelConfig: null, // 将在后续根据selectedModel更新 prompt: null, // 将在发送消息时填充 instructions: null, // 默认无额外指令 selectedText: null, // 聊天不涉及选中文本 contextSelections: contextSelectionData, enableSmartContext: true, // 默认启用智能上下文 parameters: { 'temperature': 0.7, 'maxTokens': 4000, 'memoryCutoff': 14, // 默认记忆截断 }, metadata: { 'action': 'chat', 'source': 'session_init', 'sessionId': session.id, }, ); } /// 🚀 检查并更新会话标题 void _checkAndUpdateSessionTitle(String sessionId) { // 异步执行,不阻塞主流程 Timer(const Duration(milliseconds: 500), () async { try { AppLogger.i('ChatBloc', '异步检查会话标题更新: sessionId=$sessionId'); // 🚀 这里需要从当前状态获取novelId String? novelId; if (state is ChatSessionActive) { final currentState = state as ChatSessionActive; if (currentState.session.id == sessionId) { novelId = currentState.session.novelId; } } final updatedSession = await repository.getSession(_userId, sessionId, novelId: novelId); if (state is ChatSessionActive) { final currentState = state as ChatSessionActive; if (currentState.session.id == sessionId && currentState.session.title != updatedSession.title) { AppLogger.i('ChatBloc', '会话标题已更新: ${currentState.session.title} -> ${updatedSession.title}'); add(UpdateChatTitle(newTitle: updatedSession.title)); } } } catch (e) { AppLogger.w('ChatBloc', '检查会话标题更新失败: $e'); } }); } }