Files
2025-09-10 00:07:52 +08:00

1558 lines
65 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

import 'dart:async';
import 'package:ainoval/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<ChatEvent, ChatState> {
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<LoadChatSessions>(_onLoadChatSessions, transformer: restartable());
on<CreateChatSession>(_onCreateChatSession);
on<SelectChatSession>(_onSelectChatSession);
on<SendMessage>(_onSendMessage); // 🚀 临时移除sequential转换器进行调试
on<LoadMoreMessages>(_onLoadMoreMessages);
on<UpdateChatTitle>(_onUpdateChatTitle);
on<ExecuteAction>(_onExecuteAction);
on<DeleteChatSession>(_onDeleteChatSession);
on<CancelOngoingRequest>(_onCancelRequest);
on<UpdateChatContext>(_onUpdateChatContext);
on<UpdateChatModel>(_onUpdateChatModel);
on<LoadContextData>(_onLoadContextData);
on<CacheSettingsData>(_onCacheSettingsData);
on<CacheSnippetsData>(_onCacheSnippetsData);
on<UpdateChatConfiguration>(_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<String, Map<String, UniversalAIRequest>> _sessionConfigs = {};
// 用于跟踪活动的流订阅,以便可以取消它们
// StreamSubscription? _sessionsSubscription;
// StreamSubscription? _messagesSubscription;
// 用于取消正在进行的消息生成请求
StreamSubscription? _sendMessageSubscription;
StreamSubscription? _aiConfigSubscription;
// 标记用户是否请求取消,用于在流式处理过程中提前退出
bool _cancelRequested = false;
// 临时存储上下文数据,用于在非活动状态时保存加载的数据
List<dynamic> _tempCachedSettings = [];
List<dynamic> _tempCachedSettingGroups = [];
List<dynamic> _tempCachedSnippets = [];
@override
Future<void> 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<void> _onLoadChatSessions(
LoadChatSessions event, Emitter<ChatState> emit) async {
AppLogger.i('ChatBloc',
'[Event Start] _onLoadChatSessions for novel ${event.novelId}');
emit(ChatSessionsLoading());
final List<ChatSession> sessions = []; // 不再需要局部变量
try {
// 🚀 传递novelId给repository
final stream = repository.fetchUserSessions(_userId, novelId: event.novelId);
// 使用 await emit.forEach 处理流
await emit.forEach<ChatSession>(
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<void> _onCreateChatSession(
CreateChatSession event, Emitter<ChatState> 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<ChatSession>.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<void> _onSelectChatSession(
SelectChatSession event, Emitter<ChatState> 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<String, dynamic> updatedMeta = Map<String, dynamic>.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<ChatMessage> 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<ChatMessage>(
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<void> _onSendMessage(
SendMessage event, Emitter<ChatState> 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<ChatMessage>.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<void> _onLoadMoreMessages(
LoadMoreMessages event, Emitter<ChatState> emit) async {
// TODO: 实现加载更多历史消息的逻辑
// 需要修改 repository.getMessageHistory 以支持分页或 "before" 参数
// 然后将获取到的旧消息插入到当前消息列表的前面
AppLogger.w('ChatBloc', '_onLoadMoreMessages 尚未实现');
}
Future<void> _onUpdateChatTitle(
UpdateChatTitle event, Emitter<ChatState> 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<void> _onExecuteAction(
ExecuteAction event, Emitter<ChatState> 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<void> _onDeleteChatSession(
DeleteChatSession event, Emitter<ChatState> emit) async {
List<ChatSession>? 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<void> _onCancelRequest(
CancelOngoingRequest event, Emitter<ChatState> 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<ChatMessage>.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<void> _onUpdateChatContext(
UpdateChatContext event, Emitter<ChatState> emit) async {
if (state is ChatSessionActive) {
final currentState = state as ChatSessionActive;
emit(currentState.copyWith(
context: event.context,
));
}
}
// 修改:处理流式响应的辅助方法,接收 placeholderId 和 chatConfig
// 使用 await emit.forEach 重构
Future<void> _handleStreamedResponse(
Emitter<ChatState> 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<ChatMessage>(
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<ChatMessage>.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<ChatMessage>.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<ChatMessage>.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<ChatMessage> _markPlaceholderAsError(List<ChatMessage> messages,
String placeholderId, String bufferedContent, String errorMessage) {
final listCopy = List<ChatMessage>.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<void> _onUpdateChatModel(
UpdateChatModel event, Emitter<ChatState> 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<void> _onLoadContextData(
LoadContextData event,
Emitter<ChatState> 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<String, dynamic>;
final snippetsData = futures[1] as List<NovelSnippet>;
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<void> _onCacheSettingsData(
CacheSettingsData event,
Emitter<ChatState> emit,
) async {
final currentState = state;
if (currentState is ChatSessionActive) {
emit(currentState.copyWith(
cachedSettings: event.settings,
cachedSettingGroups: event.settingGroups,
));
}
}
/// 缓存片段数据
Future<void> _onCacheSnippetsData(
CacheSnippetsData event,
Emitter<ChatState> emit,
) async {
final currentState = state;
if (currentState is ChatSessionActive) {
emit(currentState.copyWith(
cachedSnippets: event.snippets,
));
}
}
/// 加载设定数据
Future<Map<String, dynamic>> _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<NovelSettingItem>,
'groups': futures[1] as List<SettingGroup>,
};
} catch (e) {
AppLogger.e('ChatBloc', '加载设定数据失败', e);
return {
'settings': <NovelSettingItem>[],
'groups': <SettingGroup>[],
};
}
}
/// 加载片段数据
Future<List<NovelSnippet>> _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 <NovelSnippet>[];
}
}
/// 🚀 更新聊天配置
Future<void> _onUpdateChatConfiguration(
UpdateChatConfiguration event, Emitter<ChatState> 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');
}
});
}
}