马良AI写作初始化仓库

This commit is contained in:
邓滨杰
2025-09-10 00:07:52 +08:00
parent 3c06bb1a03
commit 39c0f8840f
1309 changed files with 318528 additions and 0 deletions

View File

@@ -0,0 +1,316 @@
import 'dart:async';
import 'package:ainoval/models/import_status.dart';
import 'package:ainoval/models/user_ai_model_config_model.dart';
import 'package:ainoval/services/api_service/repositories/novel_repository.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:file_picker/file_picker.dart';
part 'novel_import_event.dart';
part 'novel_import_state.dart';
/// 小说导入Bloc - 支持新的三步导入流程
class NovelImportBloc extends Bloc<NovelImportEvent, NovelImportState> {
/// 创建小说导入Bloc
NovelImportBloc({required this.novelRepository})
: super(NovelImportInitial()) {
// 第一步:上传文件获取预览
on<UploadFileForPreview>(_onUploadFileForPreview);
// 第二步:获取导入预览
on<GetImportPreview>(_onGetImportPreview);
// 第三步:确认并开始导入
on<ConfirmAndStartImport>(_onConfirmAndStartImport);
// 导入状态更新
on<ImportStatusUpdate>(_onImportStatusUpdate);
// 重置状态
on<ResetImportState>(_onResetImportState);
// 清理预览会话
on<CleanupPreviewSession>(_onCleanupPreviewSession);
// 传统导入(向后兼容)
on<ImportNovelFile>(_onImportNovelFile);
}
/// 小说仓库
final NovelRepository novelRepository;
/// 导入状态订阅
StreamSubscription<ImportStatus>? _importStatusSubscription;
/// 处理上传文件获取预览事件
Future<void> _onUploadFileForPreview(
UploadFileForPreview event, Emitter<NovelImportState> emit) async {
emit(NovelImportUploading(message: '正在上传文件...'));
try {
// 选择文件
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['txt'],
withData: true,
);
if (result == null || result.files.isEmpty) {
emit(NovelImportInitial());
return;
}
final file = result.files.first;
final fileBytes = file.bytes;
final fileName = file.name;
if (fileBytes == null) {
emit(NovelImportFailure(message: '无法读取文件数据'));
return;
}
emit(NovelImportUploading(message: '正在上传文件到服务器...'));
// 上传文件并获取预览会话ID
final previewSessionId = await novelRepository.uploadFileForPreview(fileBytes, fileName);
emit(NovelImportFileUploaded(
previewSessionId: previewSessionId,
fileName: fileName,
fileSize: fileBytes.length,
));
} catch (e) {
AppLogger.e('NovelImportBloc', '上传文件失败', e);
emit(NovelImportFailure(message: '上传文件失败: ${e.toString()}'));
}
}
/// 处理获取导入预览事件
Future<void> _onGetImportPreview(
GetImportPreview event, Emitter<NovelImportState> emit) async {
emit(NovelImportLoadingPreview(message: '正在解析文件...'));
try {
// 获取导入预览
final responseData = await novelRepository.getImportPreview(
fileSessionId: event.previewSessionId,
customTitle: event.customTitle,
chapterLimit: event.chapterLimit,
enableSmartContext: event.enableSmartContext,
enableAISummary: event.enableAISummary,
aiConfigId: event.aiConfigId,
previewChapterCount: event.previewChapterCount,
);
// 转换为ImportPreviewResponse对象
final previewResponse = ImportPreviewResponse.fromJson(responseData);
emit(NovelImportPreviewReady(
previewResponse: previewResponse,
fileName: event.fileName,
));
} catch (e) {
AppLogger.e('NovelImportBloc', '获取导入预览失败', e);
emit(NovelImportFailure(message: '获取预览失败: ${e.toString()}'));
}
}
/// 处理确认并开始导入事件
Future<void> _onConfirmAndStartImport(
ConfirmAndStartImport event, Emitter<NovelImportState> emit) async {
emit(NovelImportInProgress(status: 'CONFIRMING', message: '确认导入配置...'));
try {
// 确认并开始导入
final jobId = await novelRepository.confirmAndStartImport(
previewSessionId: event.previewSessionId,
finalTitle: event.finalTitle,
selectedChapterIndexes: event.selectedChapterIndexes,
enableSmartContext: event.enableSmartContext,
enableAISummary: event.enableAISummary,
aiConfigId: event.aiConfigId,
);
emit(NovelImportInProgress(
status: 'PROCESSING', message: '开始处理...', jobId: jobId));
// 订阅导入状态更新
_importStatusSubscription?.cancel();
_importStatusSubscription = novelRepository.getImportStatus(jobId).listen(
(importStatus) {
add(ImportStatusUpdate(
status: importStatus.status,
message: importStatus.message,
jobId: jobId,
progress: importStatus.progress,
currentStep: importStatus.currentStep,
processedChapters: importStatus.processedChapters,
totalChapters: importStatus.totalChapters,
));
},
onError: (error) {
AppLogger.e('NovelImportBloc', '监听导入状态流错误', error);
add(ImportStatusUpdate(
status: 'FAILED',
message: '监听导入状态失败: ${error.toString()}',
jobId: jobId,
));
},
onDone: () {
AppLogger.i('NovelImportBloc', '导入状态流已关闭');
},
);
} catch (e) {
AppLogger.e('NovelImportBloc', '确认导入失败', e);
emit(NovelImportFailure(message: '确认导入失败: ${e.toString()}'));
}
}
/// 处理导入状态更新事件
void _onImportStatusUpdate(
ImportStatusUpdate event, Emitter<NovelImportState> emit) {
if (event.status == 'COMPLETED') {
emit(NovelImportSuccess(message: event.message));
_importStatusSubscription?.cancel();
_importStatusSubscription = null;
} else if (event.status == 'FAILED' || event.status == 'ERROR') {
emit(NovelImportFailure(message: event.message));
_importStatusSubscription?.cancel();
_importStatusSubscription = null;
} else {
emit(NovelImportInProgress(
status: event.status,
message: event.message,
jobId: event.jobId,
progress: event.progress,
currentStep: event.currentStep,
processedChapters: event.processedChapters,
totalChapters: event.totalChapters,
));
}
}
/// 处理清理预览会话事件
Future<void> _onCleanupPreviewSession(
CleanupPreviewSession event, Emitter<NovelImportState> emit) async {
try {
await novelRepository.cleanupPreviewSession(event.previewSessionId);
AppLogger.i('NovelImportBloc', '预览会话已清理: ${event.previewSessionId}');
} catch (e) {
AppLogger.e('NovelImportBloc', '清理预览会话失败', e);
}
}
/// 重置导入状态
void _onResetImportState(
ResetImportState event, Emitter<NovelImportState> emit) async {
try {
// 如果已经不是InProgress状态不再重复取消
if (state is! NovelImportInProgress) {
emit(NovelImportInitial());
return;
}
// 记录当前JobId避免重复取消
final currentState = state as NovelImportInProgress;
final jobId = currentState.jobId;
// 立即切换到取消中状态,防止重复操作
emit(NovelImportInProgress(
status: 'CANCELLING',
message: '正在取消导入...',
jobId: jobId
));
// 取消订阅
await _importStatusSubscription?.cancel();
_importStatusSubscription = null;
// 如果有JobId尝试取消任务
if (jobId != null) {
// 通知服务器取消任务
final success = await novelRepository.cancelImport(jobId);
AppLogger.i('NovelImportBloc',
'导入任务取消${success ? '成功' : '失败或已完成'}: $jobId');
}
// 重置状态
emit(NovelImportInitial());
} catch (e) {
AppLogger.e('NovelImportBloc', '重置导入状态时出错', e);
// 即使出错,也要确保状态被重置
emit(NovelImportInitial());
}
}
/// 处理传统导入小说文件事件(向后兼容)
Future<void> _onImportNovelFile(
ImportNovelFile event, Emitter<NovelImportState> emit) async {
emit(NovelImportInProgress(status: 'PREPARING', message: '准备中...'));
try {
// 选择文件
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['txt'],
withData: true,
);
if (result == null || result.files.isEmpty) {
emit(NovelImportInitial());
return;
}
final file = result.files.first;
final fileBytes = file.bytes;
final fileName = file.name;
if (fileBytes == null) {
emit(NovelImportFailure(message: '无法读取文件数据'));
return;
}
emit(NovelImportInProgress(status: 'UPLOADING', message: '上传中...'));
// 上传文件并获取任务ID
final jobId = await novelRepository.importNovel(fileBytes, fileName);
emit(NovelImportInProgress(
status: 'PROCESSING', message: '处理中...', jobId: jobId));
// 订阅导入状态更新
_importStatusSubscription?.cancel();
_importStatusSubscription = novelRepository.getImportStatus(jobId).listen(
(importStatus) {
add(ImportStatusUpdate(
status: importStatus.status,
message: importStatus.message,
jobId: jobId,
));
},
onError: (error) {
AppLogger.e('NovelImportBloc', '监听导入状态流错误', error);
add(ImportStatusUpdate(
status: 'FAILED',
message: '监听导入状态失败: ${error.toString()}',
jobId: jobId,
));
},
onDone: () {
AppLogger.i('NovelImportBloc', '导入状态流已关闭');
},
);
} catch (e) {
AppLogger.e('NovelImportBloc', '导入小说失败', e);
emit(NovelImportFailure(message: '导入失败: ${e.toString()}'));
}
}
@override
Future<void> close() {
_importStatusSubscription?.cancel();
return super.close();
}
}

View File

@@ -0,0 +1,132 @@
part of 'novel_import_bloc.dart';
/// 小说导入事件基类
abstract class NovelImportEvent extends Equatable {
const NovelImportEvent();
@override
List<Object?> get props => [];
}
/// 第一步:上传文件获取预览
class UploadFileForPreview extends NovelImportEvent {
const UploadFileForPreview();
}
/// 第二步:获取导入预览
class GetImportPreview extends NovelImportEvent {
const GetImportPreview({
required this.previewSessionId,
this.customTitle,
this.chapterLimit,
this.enableSmartContext = true,
this.enableAISummary = false,
this.aiConfigId,
this.previewChapterCount = 10,
required this.fileName,
});
final String previewSessionId;
final String? customTitle;
final int? chapterLimit;
final bool enableSmartContext;
final bool enableAISummary;
final String? aiConfigId;
final int previewChapterCount;
final String fileName;
@override
List<Object?> get props => [
previewSessionId,
customTitle,
chapterLimit,
enableSmartContext,
enableAISummary,
aiConfigId,
previewChapterCount,
fileName,
];
}
/// 第三步:确认并开始导入
class ConfirmAndStartImport extends NovelImportEvent {
const ConfirmAndStartImport({
required this.previewSessionId,
required this.finalTitle,
this.selectedChapterIndexes,
this.enableSmartContext = true,
this.enableAISummary = false,
this.aiConfigId,
});
final String previewSessionId;
final String finalTitle;
final List<int>? selectedChapterIndexes;
final bool enableSmartContext;
final bool enableAISummary;
final String? aiConfigId;
@override
List<Object?> get props => [
previewSessionId,
finalTitle,
selectedChapterIndexes,
enableSmartContext,
enableAISummary,
aiConfigId,
];
}
/// 导入状态更新事件
class ImportStatusUpdate extends NovelImportEvent {
const ImportStatusUpdate({
required this.status,
required this.message,
required this.jobId,
this.progress,
this.currentStep,
this.processedChapters,
this.totalChapters,
});
final String status;
final String message;
final String jobId;
final double? progress;
final String? currentStep;
final int? processedChapters;
final int? totalChapters;
@override
List<Object?> get props => [
status,
message,
jobId,
progress,
currentStep,
processedChapters,
totalChapters,
];
}
/// 重置导入状态
class ResetImportState extends NovelImportEvent {
const ResetImportState();
}
/// 清理预览会话
class CleanupPreviewSession extends NovelImportEvent {
const CleanupPreviewSession({
required this.previewSessionId,
});
final String previewSessionId;
@override
List<Object?> get props => [previewSessionId];
}
/// 传统导入小说文件事件(向后兼容)
class ImportNovelFile extends NovelImportEvent {
const ImportNovelFile();
}

View File

@@ -0,0 +1,231 @@
part of 'novel_import_bloc.dart';
/// 小说导入状态基类
abstract class NovelImportState extends Equatable {
const NovelImportState();
@override
List<Object?> get props => [];
}
/// 初始状态
class NovelImportInitial extends NovelImportState {}
/// 第一步:上传文件中
class NovelImportUploading extends NovelImportState {
const NovelImportUploading({required this.message});
final String message;
@override
List<Object?> get props => [message];
}
/// 第一步完成:文件已上传
class NovelImportFileUploaded extends NovelImportState {
const NovelImportFileUploaded({
required this.previewSessionId,
required this.fileName,
required this.fileSize,
});
final String previewSessionId;
final String fileName;
final int fileSize;
@override
List<Object?> get props => [previewSessionId, fileName, fileSize];
}
/// 第二步:加载预览中
class NovelImportLoadingPreview extends NovelImportState {
const NovelImportLoadingPreview({required this.message});
final String message;
@override
List<Object?> get props => [message];
}
/// 第二步完成:预览准备就绪
class NovelImportPreviewReady extends NovelImportState {
const NovelImportPreviewReady({
required this.previewResponse,
required this.fileName,
});
final ImportPreviewResponse previewResponse;
final String fileName;
@override
List<Object?> get props => [previewResponse, fileName];
}
/// 第三步:导入进行中
class NovelImportInProgress extends NovelImportState {
const NovelImportInProgress({
required this.status,
required this.message,
this.jobId,
this.progress,
this.currentStep,
this.processedChapters,
this.totalChapters,
});
final String status;
final String message;
final String? jobId;
final double? progress;
final String? currentStep;
final int? processedChapters;
final int? totalChapters;
@override
List<Object?> get props => [
status,
message,
jobId,
progress,
currentStep,
processedChapters,
totalChapters,
];
}
/// 导入成功
class NovelImportSuccess extends NovelImportState {
const NovelImportSuccess({required this.message});
final String message;
@override
List<Object?> get props => [message];
}
/// 导入失败
class NovelImportFailure extends NovelImportState {
const NovelImportFailure({required this.message});
final String message;
@override
List<Object?> get props => [message];
}
/// 导入预览响应数据类
class ImportPreviewResponse {
const ImportPreviewResponse({
required this.previewSessionId,
required this.detectedTitle,
required this.totalChapterCount,
required this.chapterPreviews,
required this.totalWordCount,
this.aiEstimation,
this.warnings = const [],
});
final String previewSessionId;
final String detectedTitle;
final int totalChapterCount;
final List<ChapterPreview> chapterPreviews;
final int totalWordCount;
final AIEstimation? aiEstimation;
final List<String> warnings;
factory ImportPreviewResponse.fromJson(Map<String, dynamic> json) {
return ImportPreviewResponse(
previewSessionId: json['previewSessionId'] as String,
detectedTitle: json['detectedTitle'] as String,
totalChapterCount: json['totalChapterCount'] as int,
chapterPreviews: (json['chapterPreviews'] as List<dynamic>)
.map((e) => ChapterPreview.fromJson(e as Map<String, dynamic>))
.toList(),
totalWordCount: json['totalWordCount'] as int,
aiEstimation: json['aiEstimation'] != null
? AIEstimation.fromJson(json['aiEstimation'] as Map<String, dynamic>)
: null,
warnings: json['warnings'] != null
? List<String>.from(json['warnings'] as List<dynamic>)
: const [],
);
}
}
/// 章节预览数据类
class ChapterPreview {
const ChapterPreview({
required this.chapterIndex,
required this.title,
required this.contentPreview,
required this.fullContentLength,
required this.wordCount,
this.selected = true,
});
final int chapterIndex;
final String title;
final String contentPreview;
final int fullContentLength;
final int wordCount;
final bool selected;
factory ChapterPreview.fromJson(Map<String, dynamic> json) {
return ChapterPreview(
chapterIndex: json['chapterIndex'] as int,
title: json['title'] as String,
contentPreview: json['contentPreview'] as String,
fullContentLength: json['fullContentLength'] as int,
wordCount: json['wordCount'] as int,
selected: json['selected'] as bool? ?? true,
);
}
ChapterPreview copyWith({
int? chapterIndex,
String? title,
String? contentPreview,
int? fullContentLength,
int? wordCount,
bool? selected,
}) {
return ChapterPreview(
chapterIndex: chapterIndex ?? this.chapterIndex,
title: title ?? this.title,
contentPreview: contentPreview ?? this.contentPreview,
fullContentLength: fullContentLength ?? this.fullContentLength,
wordCount: wordCount ?? this.wordCount,
selected: selected ?? this.selected,
);
}
}
/// AI估算数据类
class AIEstimation {
const AIEstimation({
required this.supported,
this.estimatedTokens,
this.estimatedCost,
this.estimatedTimeMinutes,
this.selectedModel,
this.limitations,
});
final bool supported;
final int? estimatedTokens;
final double? estimatedCost;
final int? estimatedTimeMinutes;
final String? selectedModel;
final String? limitations;
factory AIEstimation.fromJson(Map<String, dynamic> json) {
return AIEstimation(
supported: json['supported'] as bool,
estimatedTokens: json['estimatedTokens'] as int?,
estimatedCost: json['estimatedCost'] as double?,
estimatedTimeMinutes: json['estimatedTimeMinutes'] as int?,
selectedModel: json['selectedModel'] as String?,
limitations: json['limitations'] as String?,
);
}
}