1558 lines
65 KiB
Dart
1558 lines
65 KiB
Dart
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');
|
||
}
|
||
});
|
||
}
|
||
}
|