马良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,144 @@
/// LLM可观测性Repository接口
/// 用于管理后台LLM调用日志的查询和分析
import '../../../../models/admin/llm_observability_models.dart';
abstract class LLMObservabilityRepository {
// ==================== 日志查询 ====================
/// 获取所有LLM调用日志
Future<PagedResponse<LLMTrace>> getAllTraces({
int page = 0,
int size = 20,
String sortBy = 'timestamp',
String sortDir = 'desc',
});
/// 根据用户ID获取LLM调用日志
Future<PagedResponse<LLMTrace>> getTracesByUserId(
String userId, {
int page = 0,
int size = 20,
});
/// 根据提供商获取LLM调用日志
Future<PagedResponse<LLMTrace>> getTracesByProvider(
String provider, {
int page = 0,
int size = 20,
});
/// 根据模型名称获取LLM调用日志
Future<PagedResponse<LLMTrace>> getTracesByModel(
String modelName, {
int page = 0,
int size = 20,
});
/// 根据时间范围获取LLM调用日志
Future<PagedResponse<LLMTrace>> getTracesByTimeRange(
DateTime startTime,
DateTime endTime, {
int page = 0,
int size = 20,
});
/// 搜索LLM调用日志
Future<PagedResponse<LLMTrace>> searchTraces(
LLMTraceSearchCriteria criteria, {
String? businessType,
String? correlationId,
String? traceId,
String? type,
String? tag,
});
/// 游标分页获取LLM调用日志
Future<CursorPageResponse<LLMTrace>> getTracesByCursor({
String? cursor,
int limit = 50,
String? userId,
String? provider,
String? model,
String? sessionId,
bool? hasError,
String? businessType,
String? correlationId,
String? traceId,
String? type,
String? tag,
DateTime? startTime,
DateTime? endTime,
});
/// 获取单个LLM调用日志详情
Future<LLMTrace?> getTraceById(String traceId);
// ==================== 统计分析 ====================
/// 获取LLM调用统计概览
Future<Map<String, dynamic>> getOverviewStatistics({
DateTime? startTime,
DateTime? endTime,
});
/// 获取提供商统计信息
Future<List<ProviderStatistics>> getProviderStatistics({
DateTime? startTime,
DateTime? endTime,
});
/// 获取模型统计信息
Future<List<ModelStatistics>> getModelStatistics({
DateTime? startTime,
DateTime? endTime,
});
/// 获取用户统计信息
Future<List<UserStatistics>> getUserStatistics({
DateTime? startTime,
DateTime? endTime,
});
/// 获取错误统计信息
Future<List<ErrorStatistics>> getErrorStatistics({
DateTime? startTime,
DateTime? endTime,
});
/// 获取性能统计信息
Future<PerformanceStatistics> getPerformanceStatistics({
DateTime? startTime,
DateTime? endTime,
});
/// 获取趋势数据(按时间分桶)
Future<Map<String, dynamic>> getTrends({
String? metric,
String? groupBy,
String? businessType,
String? model,
String? provider,
String interval = 'hour',
DateTime? startTime,
DateTime? endTime,
});
// ==================== 导出功能 ====================
/// 导出LLM调用日志
Future<List<LLMTrace>> exportTraces({
Map<String, dynamic>? filterCriteria,
});
// ==================== 系统管理 ====================
/// 清理旧日志
Future<Map<String, dynamic>> cleanupOldTraces(DateTime beforeTime);
/// 获取系统健康状态
Future<SystemHealthStatus> getSystemHealth();
/// 获取数据库状态
Future<Map<String, dynamic>> getDatabaseStatus();
}

View File

@@ -0,0 +1,122 @@
import 'package:ainoval/models/preset_models.dart';
/// AI预设仓储接口
abstract class AIPresetRepository {
/// 创建预设
/// [request] 创建预设请求
/// 返回创建的预设
Future<AIPromptPreset> createPreset(CreatePresetRequest request);
/// 获取用户的所有预设
/// [userId] 用户ID如果为null则获取当前用户的预设
/// 返回预设列表
Future<List<AIPromptPreset>> getUserPresets({String? userId, String featureType = 'AI_CHAT'});
/// 搜索预设
/// [params] 搜索参数
/// 返回匹配的预设列表
Future<List<AIPromptPreset>> searchPresets(PresetSearchParams params);
/// 根据ID获取预设
/// [presetId] 预设ID
/// 返回预设详情
Future<AIPromptPreset> getPresetById(String presetId);
/// 覆盖更新预设(完整对象)
/// [preset] 完整的预设对象
/// 返回更新后的预设
Future<AIPromptPreset> overwritePreset(AIPromptPreset preset);
/// 更新预设信息
/// [presetId] 预设ID
/// [request] 更新请求
/// 返回更新后的预设
Future<AIPromptPreset> updatePresetInfo(String presetId, UpdatePresetInfoRequest request);
/// 更新预设提示词
/// [presetId] 预设ID
/// [request] 更新提示词请求
/// 返回更新后的预设
Future<AIPromptPreset> updatePresetPrompts(String presetId, UpdatePresetPromptsRequest request);
/// 删除预设
/// [presetId] 预设ID
Future<void> deletePreset(String presetId);
/// 复制预设
/// [presetId] 源预设ID
/// [request] 复制请求
/// 返回新创建的预设
Future<AIPromptPreset> duplicatePreset(String presetId, DuplicatePresetRequest request);
/// 切换收藏状态
/// [presetId] 预设ID
/// 返回更新后的预设
Future<AIPromptPreset> toggleFavorite(String presetId);
/// 记录预设使用
/// [presetId] 预设ID
Future<void> recordPresetUsage(String presetId);
/// 获取预设统计信息
/// 返回统计信息
Future<PresetStatistics> getPresetStatistics();
/// 获取收藏的预设
/// [novelId] 小说ID如果为null则获取全局预设
/// [featureType] 功能类型,如果指定则只返回该类型的预设
/// 返回收藏预设列表
Future<List<AIPromptPreset>> getFavoritePresets({String? novelId, String? featureType});
/// 获取最近使用的预设
/// [limit] 返回数量限制默认10个
/// [novelId] 小说ID如果为null则获取全局预设
/// [featureType] 功能类型,如果指定则只返回该类型的预设
/// 返回最近使用预设列表
Future<List<AIPromptPreset>> getRecentlyUsedPresets({int limit = 10, String? novelId, String? featureType});
/// 根据功能类型获取预设
/// [featureType] 功能类型
/// 返回指定功能类型的预设列表
Future<List<AIPromptPreset>> getPresetsByFeatureType(String featureType);
// ============ 新增:系统预设管理接口 ============
/// 获取系统预设列表
/// [featureType] 功能类型,如果指定则只返回该类型的系统预设
/// 返回系统预设列表
Future<List<AIPromptPreset>> getSystemPresets({String? featureType});
/// 获取快捷访问预设
/// [featureType] 功能类型,如果指定则只返回该类型的快捷访问预设
/// [novelId] 小说ID如果为null则获取全局快捷访问预设
/// 返回快捷访问预设列表
Future<List<AIPromptPreset>> getQuickAccessPresets({String? featureType, String? novelId});
/// 切换预设的快捷访问状态
/// [presetId] 预设ID
/// 返回更新后的预设
Future<AIPromptPreset> toggleQuickAccess(String presetId);
/// 批量获取预设
/// [presetIds] 预设ID列表
/// 返回预设列表
Future<List<AIPromptPreset>> getPresetsByIds(List<String> presetIds);
/// 获取用户预设按功能类型分组
/// [userId] 用户ID如果为null则获取当前用户的预设
/// 返回功能类型到预设列表的映射
Future<Map<String, List<AIPromptPreset>>> getUserPresetsByFeatureType({String? userId});
/// 获取用户在指定功能类型下的预设管理信息
/// [featureType] 功能类型
/// [novelId] 小说ID可选
/// 返回该功能类型下的完整预设管理信息
Future<Map<String, dynamic>> getFeatureTypePresetManagement(String featureType, {String? novelId});
/// 获取功能预设列表(收藏、最近使用、推荐)
/// [featureType] 功能类型
/// [novelId] 小说ID可选
/// 返回分类的预设列表
Future<PresetListResponse> getFeaturePresetList(String featureType, {String? novelId});
}

View File

@@ -0,0 +1,48 @@
import 'package:ainoval/models/analytics_data.dart';
abstract class AnalyticsRepository {
/// 获取用户分析概览数据
Future<AnalyticsData> getAnalyticsOverview();
/// 获取Token使用趋势数据
/// [viewMode] 查看模式daily, monthly, cumulative, range
/// [startDate] 开始日期range模式使用
/// [endDate] 结束日期range模式使用
Future<List<TokenUsageData>> getTokenUsageTrend({
required AnalyticsViewMode viewMode,
DateTime? startDate,
DateTime? endDate,
});
/// 获取功能使用统计数据
/// [viewMode] 查看模式daily, monthly, range
/// [startDate] 开始日期range模式使用
/// [endDate] 结束日期range模式使用
Future<List<FunctionUsageData>> getFunctionUsageStats({
required AnalyticsViewMode viewMode,
DateTime? startDate,
DateTime? endDate,
});
/// 获取大模型使用占比数据(按模型名聚合)
/// [viewMode] 查看模式daily, monthly, range
/// [startDate] 开始日期range模式使用
/// [endDate] 结束日期range模式使用
Future<List<ModelUsageData>> getModelUsageStats({
required AnalyticsViewMode viewMode,
DateTime? startDate,
DateTime? endDate,
});
/// 获取Token使用记录列表
/// [limit] 返回记录数量限制
/// [offset] 偏移量
Future<List<TokenUsageRecord>> getTokenUsageRecords({
int limit = 20,
int offset = 0,
});
/// 获取今日Token使用汇总
Future<Map<String, dynamic>> getTodayTokenSummary();
}

View File

@@ -0,0 +1,75 @@
import 'package:ainoval/models/chat_models.dart';
import 'package:ainoval/models/ai_request_models.dart';
/// 聊天仓库接口
///
/// 定义与聊天相关的所有API操作
abstract class ChatRepository {
/// 获取用户的所有会话
Stream<ChatSession> fetchUserSessions(String userId, {String? novelId});
/// 创建新的聊天会话
Future<ChatSession> createSession({
required String userId,
required String novelId,
String? modelName,
Map<String, dynamic>? metadata,
});
/// 获取特定会话详情包含AI配置
Future<ChatSession> getSession(String userId, String sessionId, {String? novelId});
/// 获取会话的AI配置
Future<UniversalAIRequest?> getSessionAIConfig(String userId, String sessionId, {String? novelId});
/// 更新会话信息
Future<ChatSession> updateSession({
required String userId,
required String sessionId,
required Map<String, dynamic> updates,
String? novelId,
});
/// 删除会话
Future<void> deleteSession(String userId, String sessionId, {String? novelId});
/// 发送消息并获取响应
/// 返回完整的 AI ChatMessage 对象
Future<ChatMessage> sendMessage({
required String userId,
required String sessionId,
required String content,
UniversalAIRequest? config,
Map<String, dynamic>? metadata,
String? configId,
String? novelId,
});
/// 流式发送消息并获取响应
/// 流式返回 AI ChatMessage 对象片段
Stream<ChatMessage> streamMessage({
required String userId,
required String sessionId,
required String content,
UniversalAIRequest? config,
Map<String, dynamic>? metadata,
String? configId,
String? novelId,
});
/// 获取会话消息历史
Stream<ChatMessage> getMessageHistory(String userId, String sessionId,
{int limit = 100, String? novelId});
/// 获取特定消息
Future<ChatMessage> getMessage(String userId, String messageId);
/// 删除消息
Future<void> deleteMessage(String userId, String messageId);
/// 获取会话消息数量
Future<int> countSessionMessages(String sessionId);
/// 获取用户会话数量
Future<int> countUserSessions(String userId, {String? novelId});
}

View File

@@ -0,0 +1,7 @@
import '../../../models/user_credit.dart';
/// 用户积分仓库接口
abstract interface class CreditRepository {
/// 获取当前用户的积分余额
Future<UserCredit> getUserCredits();
}

View File

@@ -0,0 +1,233 @@
import 'dart:async';
import 'package:ainoval/models/editor_content.dart';
import 'package:ainoval/models/editor_settings.dart';
import 'package:ainoval/models/novel_structure.dart';
import 'package:ainoval/models/chapters_for_preload_dto.dart';
import 'package:ainoval/services/local_storage_service.dart';
/// 编辑器仓库接口
///
/// 定义与编辑器相关的所有API操作
abstract class EditorRepository {
/// 获取本地存储服务
LocalStorageService getLocalStorageService();
/// 获取小说
Future<Novel?> getNovel(String novelId);
/// 获取小说详情(分页加载场景)
/// 基于上次编辑章节为中心,获取前后指定数量的章节及其场景内容
Future<Novel?> getNovelWithPaginatedScenes(String novelId, String lastEditedChapterId, {int chaptersLimit = 5});
/// 获取小说详情(一次性加载所有场景)
/// 一次性获取小说的所有章节及其场景内容
Future<Novel?> getNovelWithAllScenes(String novelId);
/// 加载更多章节场景
/// 根据方向(向上或向下)加载更多章节的场景内容
Future<Map<String, List<Scene>>> loadMoreScenes(String novelId, String? actId, String fromChapterId, String direction, {int chaptersLimit = 5});
/// 保存小说数据
Future<bool> saveNovel(Novel novel);
/// 获取场景内容
Future<Scene?> getSceneContent(
String novelId, String actId, String chapterId, String sceneId);
/// 保存场景内容
Future<Scene> saveSceneContent(
String novelId,
String actId,
String chapterId,
String sceneId,
String content,
String wordCount,
Summary summary,
{bool localOnly = false}
);
/// 保存摘要
Future<Summary> saveSummary(
String novelId,
String actId,
String chapterId,
String sceneId,
String content,
);
/// 获取编辑器内容
Future<EditorContent> getEditorContent(
String novelId, String chapterId, String sceneId);
/// 保存编辑器内容
Future<void> saveEditorContent(EditorContent content);
/// 获取编辑器设置
Future<Map<String, dynamic>> getEditorSettings();
/// 保存编辑器设置
Future<void> saveEditorSettings(Map<String, dynamic> settings);
/// 获取修订历史
Future<List<Revision>> getRevisionHistory(String novelId, String chapterId);
/// 创建修订版本
Future<Revision> createRevision(
String novelId, String chapterId, Revision revision);
/// 应用修订版本
Future<void> applyRevision(
String novelId, String chapterId, String revisionId);
/// 更新小说元数据
Future<void> updateNovelMetadata({
required String novelId,
required String title,
String? author,
String? series,
});
/// 获取封面上传凭证
Future<Map<String, dynamic>> getCoverUploadCredential({
required String novelId,
required String fileName,
});
/// 更新小说封面
Future<void> updateNovelCover({
required String novelId,
required String coverUrl,
});
/// 归档小说
Future<void> archiveNovel({
required String novelId,
});
/// 删除小说
Future<void> deleteNovel({
required String novelId,
});
/// 为指定场景生成摘要
Future<String> summarizeScene(String sceneId, {String? additionalInstructions});
/// 根据摘要生成场景内容(流式)
Stream<String> generateSceneFromSummaryStream(
String novelId,
String summary,
{String? chapterId, String? additionalInstructions}
);
/// 根据摘要生成场景内容(非流式)
Future<String> generateSceneFromSummary(
String novelId,
String summary,
{String? chapterId, String? additionalInstructions}
);
/// 获取小说详情包含场景摘要适用于Plan视图
Future<Novel?> getNovelWithSceneSummaries(String novelId, {bool readOnly = false});
/// 提交自动续写任务
///
/// [novelId] 小说ID
/// [numberOfChapters] 续写章节数
/// [aiConfigIdSummary] 摘要模型配置ID
/// [aiConfigIdContent] 内容模型配置ID
/// [startContextMode] 上下文模式,可选值: AUTO, LAST_N_CHAPTERS, CUSTOM
/// [contextChapterCount] 上下文章节数仅当startContextMode为LAST_N_CHAPTERS时有效
/// [customContext] 自定义上下文仅当startContextMode为CUSTOM时有效
/// [writingStyle] 写作风格提示,可选
///
/// 返回提交的任务ID
Future<String> submitContinueWritingTask({
required String novelId,
required int numberOfChapters,
required String aiConfigIdSummary,
required String aiConfigIdContent,
required String startContextMode,
int? contextChapterCount,
String? customContext,
String? writingStyle,
});
/// 删除场景
Future<bool> deleteScene(
String novelId,
String actId,
String chapterId,
String sceneId,
);
/// 添加场景
Future<Scene?> addScene(
String novelId,
String actId,
String chapterId,
Scene scene,
);
/// 删除章节
Future<Novel?> deleteChapter(
String novelId,
String actId,
String chapterId,
);
/// 将后端返回的带场景摘要的小说数据转换为前端模型
/// 更新小说最后编辑的章节ID细粒度更新
Future<bool> updateLastEditedChapterId(String novelId, String chapterId);
/// 批量更新小说字数统计(细粒度更新)
Future<bool> updateNovelWordCounts(String novelId, Map<String, int> sceneWordCounts);
/// 智能同步小说(根据变更类型选择最优同步策略)
Future<bool> smartSyncNovel(Novel novel, {Set<String>? changedComponents});
/// 仅更新小说结构(不包含场景内容)
Future<bool> updateNovelStructure(Novel novel);
/// 批量保存场景内容(优化网络请求数量)
Future<bool> batchSaveSceneContents(
String novelId,
List<Map<String, dynamic>> sceneUpdates
);
/// 细粒度添加卷 - 只提供必要信息
Future<Act> addActFine(String novelId, String title, {String? description});
/// 细粒度添加章节 - 只提供必要信息
Future<Chapter> addChapterFine(String novelId, String actId, String title, {String? description});
/// 细粒度添加场景 - 只提供必要信息
Future<Scene> addSceneFine(String novelId, String chapterId, String title, {String? summary, int? position});
/// 细粒度批量添加场景 - 一次添加多个场景到同一章节
Future<List<Scene>> addScenesBatchFine(String novelId, String chapterId, List<Map<String, dynamic>> scenes);
/// 细粒度删除卷 - 只提供ID
Future<bool> deleteActFine(String novelId, String actId);
/// 细粒度删除章节 - 只提供ID
Future<bool> deleteChapterFine(String novelId, String actId, String chapterId);
/// 细粒度删除场景 - 只提供ID
Future<bool> deleteSceneFine(String sceneId);
/// 获取指定章节后面的章节列表(用于预加载)
///
/// [novelId] 小说ID
/// [currentChapterId] 当前章节ID
/// [chaptersLimit] 要获取的章节数量限制
/// [includeCurrentChapter] 是否包含当前章节
///
/// 返回包含章节列表和场景数据的ChaptersForPreloadDto
Future<ChaptersForPreloadDto?> fetchChaptersForPreload(
String novelId,
String currentChapterId, {
int chaptersLimit = 3,
bool includeCurrentChapter = false,
});
}

View File

@@ -0,0 +1,53 @@
import 'package:ainoval/models/admin/billing_models.dart';
import 'package:ainoval/services/api_service/base/api_client.dart';
class BillingRepositoryImpl {
final ApiClient apiClient;
BillingRepositoryImpl({required this.apiClient});
Future<List<CreditTransactionModel>> listTransactions({int page = 0, int size = 20, String? status, String? userId}) async {
final params = <String, dynamic>{
'page': page,
'size': size,
if (status != null && status.isNotEmpty) 'status': status,
if (userId != null && userId.isNotEmpty) 'userId': userId,
};
final data = await apiClient.getWithParams('/admin/billing/transactions', queryParameters: params);
if (data is List) {
return data.map((e) => CreditTransactionModel.fromJson(Map<String, dynamic>.from(e))).toList();
}
return [];
}
Future<int> countTransactions({String? status, String? userId}) async {
final params = <String, dynamic>{
if (status != null && status.isNotEmpty) 'status': status,
if (userId != null && userId.isNotEmpty) 'userId': userId,
};
final data = await apiClient.getWithParams('/admin/billing/transactions/count', queryParameters: params);
if (data is int) return data;
if (data is String) return int.tryParse(data) ?? 0;
if (data is Map<String, dynamic> && data['count'] is int) return data['count'] as int;
return 0;
}
Future<CreditTransactionModel?> getTransaction(String traceId) async {
final data = await apiClient.get('/admin/billing/transactions/$traceId');
if (data is Map<String, dynamic>) {
return CreditTransactionModel.fromJson(data);
}
return null;
}
Future<CreditTransactionModel?> reverse(String traceId, {required String operatorUserId, required String reason}) async {
final payload = {'operatorUserId': operatorUserId, 'reason': reason};
final data = await apiClient.post('/admin/billing/transactions/$traceId/reverse', data: payload);
if (data is Map<String, dynamic>) {
return CreditTransactionModel.fromJson(data);
}
return null;
}
}

View File

@@ -0,0 +1,744 @@
import '../../../../../models/admin/llm_observability_models.dart';
import '../../admin/llm_observability_repository.dart';
import '../../../base/api_client.dart';
import '../../../base/api_exception.dart';
import '../../../../../utils/logger.dart';
/// LLM可观测性仓库实现
class LLMObservabilityRepositoryImpl implements LLMObservabilityRepository {
final ApiClient _apiClient;
final String _tag = 'LLMObservabilityRepository';
LLMObservabilityRepositoryImpl({ApiClient? apiClient}) : _apiClient = apiClient ?? ApiClient();
// ==================== 日志查询 ====================
/// 获取所有LLM调用日志
Future<PagedResponse<LLMTrace>> getAllTraces({
int page = 0,
int size = 20,
String sortBy = 'timestamp',
String sortDir = 'desc',
}) async {
try {
AppLogger.d(_tag, '获取LLM调用日志: page=$page, size=$size, sortBy=$sortBy, sortDir=$sortDir');
final response = await _apiClient.getWithParams('/admin/llm-observability/traces', queryParameters: {
'page': page,
'size': size,
'sortBy': sortBy,
'sortDir': sortDir,
});
if (response is Map<String, dynamic> && response.containsKey('data')) {
return PagedResponse.fromJson(response['data'], (json) => LLMTrace.fromJson(json as Map<String, dynamic>));
} else if (response is Map<String, dynamic>) {
return PagedResponse.fromJson(response, (json) => LLMTrace.fromJson(json as Map<String, dynamic>));
} else {
throw ApiException(-1, 'LLM日志响应格式错误');
}
} catch (e) {
AppLogger.e(_tag, '获取LLM调用日志失败', e);
rethrow;
}
}
/// 游标分页获取LLM调用日志
@override
Future<CursorPageResponse<LLMTrace>> getTracesByCursor({
String? cursor,
int limit = 50,
String? userId,
String? provider,
String? model,
String? sessionId,
bool? hasError,
String? businessType,
String? correlationId,
String? traceId,
String? type,
String? tag,
DateTime? startTime,
DateTime? endTime,
}) async {
try {
final params = <String, dynamic>{
'limit': limit,
};
if (cursor != null && cursor.isNotEmpty) params['cursor'] = cursor;
if (userId != null) params['userId'] = userId;
if (provider != null) params['provider'] = provider;
if (model != null) params['model'] = model;
if (sessionId != null) params['sessionId'] = sessionId;
if (hasError != null) params['hasError'] = hasError;
if (businessType != null) params['businessType'] = businessType;
if (correlationId != null) params['correlationId'] = correlationId;
if (traceId != null) params['traceId'] = traceId;
if (type != null) params['type'] = type;
if (tag != null) params['tag'] = tag;
if (startTime != null) params['startTime'] = startTime.toIso8601String();
if (endTime != null) params['endTime'] = endTime.toIso8601String();
final response = await _apiClient.getWithParams('/admin/llm-observability/traces/cursor', queryParameters: params);
if (response is Map<String, dynamic> && response.containsKey('data')) {
return CursorPageResponse.fromJson(response['data'], (json) => LLMTrace.fromJson(json as Map<String, dynamic>));
} else if (response is Map<String, dynamic>) {
return CursorPageResponse.fromJson(response, (json) => LLMTrace.fromJson(json as Map<String, dynamic>));
} else {
throw ApiException(-1, '游标分页响应格式错误');
}
} catch (e) {
AppLogger.e(_tag, '游标分页获取LLM调用日志失败', e);
rethrow;
}
}
/// 根据用户ID获取LLM调用日志
Future<PagedResponse<LLMTrace>> getTracesByUserId(
String userId, {
int page = 0,
int size = 20,
}) async {
try {
AppLogger.d(_tag, '获取用户LLM调用日志: userId=$userId, page=$page, size=$size');
final response = await _apiClient.getWithParams('/admin/llm-observability/traces/user/$userId', queryParameters: {
'page': page,
'size': size,
});
if (response is Map<String, dynamic> && response.containsKey('data')) {
return PagedResponse.fromJson(response['data'], (json) => LLMTrace.fromJson(json as Map<String, dynamic>));
} else if (response is Map<String, dynamic>) {
return PagedResponse.fromJson(response, (json) => LLMTrace.fromJson(json as Map<String, dynamic>));
} else {
throw ApiException(-1, '用户LLM日志响应格式错误');
}
} catch (e) {
AppLogger.e(_tag, '获取用户LLM调用日志失败', e);
rethrow;
}
}
/// 根据提供商获取LLM调用日志
Future<PagedResponse<LLMTrace>> getTracesByProvider(
String provider, {
int page = 0,
int size = 20,
}) async {
try {
AppLogger.d(_tag, '获取提供商LLM调用日志: provider=$provider, page=$page, size=$size');
final response = await _apiClient.getWithParams('/admin/llm-observability/traces/provider/$provider', queryParameters: {
'page': page,
'size': size,
});
if (response is Map<String, dynamic> && response.containsKey('data')) {
return PagedResponse.fromJson(response['data'], (json) => LLMTrace.fromJson(json as Map<String, dynamic>));
} else if (response is Map<String, dynamic>) {
return PagedResponse.fromJson(response, (json) => LLMTrace.fromJson(json as Map<String, dynamic>));
} else {
throw ApiException(-1, '提供商LLM日志响应格式错误');
}
} catch (e) {
AppLogger.e(_tag, '获取提供商LLM调用日志失败', e);
rethrow;
}
}
/// 根据模型名称获取LLM调用日志
Future<PagedResponse<LLMTrace>> getTracesByModel(
String modelName, {
int page = 0,
int size = 20,
}) async {
try {
AppLogger.d(_tag, '获取模型LLM调用日志: modelName=$modelName, page=$page, size=$size');
final response = await _apiClient.getWithParams('/admin/llm-observability/traces/model/$modelName', queryParameters: {
'page': page,
'size': size,
});
if (response is Map<String, dynamic> && response.containsKey('data')) {
return PagedResponse.fromJson(response['data'], (json) => LLMTrace.fromJson(json as Map<String, dynamic>));
} else if (response is Map<String, dynamic>) {
return PagedResponse.fromJson(response, (json) => LLMTrace.fromJson(json as Map<String, dynamic>));
} else {
throw ApiException(-1, '模型LLM日志响应格式错误');
}
} catch (e) {
AppLogger.e(_tag, '获取模型LLM调用日志失败', e);
rethrow;
}
}
/// 根据时间范围获取LLM调用日志
Future<PagedResponse<LLMTrace>> getTracesByTimeRange(
DateTime startTime,
DateTime endTime, {
int page = 0,
int size = 20,
}) async {
try {
AppLogger.d(_tag, '按时间范围获取LLM调用日志: startTime=$startTime, endTime=$endTime, page=$page, size=$size');
final response = await _apiClient.getWithParams('/admin/llm-observability/traces/timerange', queryParameters: {
'startTime': startTime.toIso8601String(),
'endTime': endTime.toIso8601String(),
'page': page,
'size': size,
});
if (response is Map<String, dynamic> && response.containsKey('data')) {
return PagedResponse.fromJson(response['data'], (json) => LLMTrace.fromJson(json as Map<String, dynamic>));
} else if (response is Map<String, dynamic>) {
return PagedResponse.fromJson(response, (json) => LLMTrace.fromJson(json as Map<String, dynamic>));
} else {
throw ApiException(-1, '时间范围LLM日志响应格式错误');
}
} catch (e) {
AppLogger.e(_tag, '按时间范围获取LLM调用日志失败', e);
rethrow;
}
}
/// 搜索LLM调用日志
Future<PagedResponse<LLMTrace>> searchTraces(
LLMTraceSearchCriteria criteria, {
String? businessType,
String? correlationId,
String? traceId,
String? type,
String? tag,
}) async {
try {
AppLogger.d(_tag, '搜索LLM调用日志: criteria=$criteria');
final queryParams = <String, dynamic>{
'page': criteria.page,
'size': criteria.size,
};
if (criteria.userId != null) queryParams['userId'] = criteria.userId;
if (criteria.provider != null) queryParams['provider'] = criteria.provider;
if (criteria.model != null) queryParams['model'] = criteria.model;
if (criteria.sessionId != null) queryParams['sessionId'] = criteria.sessionId;
if (criteria.hasError != null) queryParams['hasError'] = criteria.hasError;
if (criteria.startTime != null) queryParams['startTime'] = criteria.startTime!.toIso8601String();
if (criteria.endTime != null) queryParams['endTime'] = criteria.endTime!.toIso8601String();
if (businessType != null && businessType.isNotEmpty) queryParams['businessType'] = businessType;
if (correlationId != null && correlationId.isNotEmpty) queryParams['correlationId'] = correlationId;
if (traceId != null && traceId.isNotEmpty) queryParams['traceId'] = traceId;
if (type != null && type.isNotEmpty) queryParams['type'] = type;
if (tag != null && tag.isNotEmpty) queryParams['tag'] = tag;
final response = await _apiClient.getWithParams('/admin/llm-observability/traces/search', queryParameters: queryParams);
if (response is Map<String, dynamic> && response.containsKey('data')) {
return PagedResponse.fromJson(response['data'], (json) => LLMTrace.fromJson(json as Map<String, dynamic>));
} else if (response is Map<String, dynamic>) {
return PagedResponse.fromJson(response, (json) => LLMTrace.fromJson(json as Map<String, dynamic>));
} else {
throw ApiException(-1, '搜索LLM日志响应格式错误');
}
} catch (e) {
AppLogger.e(_tag, '搜索LLM调用日志失败', e);
rethrow;
}
}
/// 获取单个LLM调用日志详情
Future<LLMTrace?> getTraceById(String traceId) async {
try {
AppLogger.d(_tag, '获取LLM调用日志详情: traceId=$traceId');
final response = await _apiClient.getWithParams('/admin/llm-observability/traces/$traceId');
if (response is Map<String, dynamic> && response.containsKey('data')) {
return LLMTrace.fromJson(response['data']);
} else if (response is Map<String, dynamic>) {
return LLMTrace.fromJson(response);
} else {
return null;
}
} catch (e) {
AppLogger.e(_tag, '获取LLM调用日志详情失败', e);
if (e is ApiException && e.statusCode == 404) {
return null;
}
rethrow;
}
}
// ==================== 统计分析 ====================
/// 获取LLM调用统计概览
Future<Map<String, dynamic>> getOverviewStatistics({
DateTime? startTime,
DateTime? endTime,
}) async {
try {
AppLogger.d(_tag, '获取LLM调用统计概览: startTime=$startTime, endTime=$endTime');
final queryParams = <String, dynamic>{};
if (startTime != null) queryParams['startTime'] = startTime.toIso8601String();
if (endTime != null) queryParams['endTime'] = endTime.toIso8601String();
final response = await _apiClient.getWithParams('/admin/llm-observability/statistics/overview', queryParameters: queryParams);
if (response is Map<String, dynamic> && response.containsKey('data')) {
return response['data'];
} else if (response is Map<String, dynamic>) {
return response;
} else {
throw ApiException(-1, '统计概览响应格式错误');
}
} catch (e) {
AppLogger.e(_tag, '获取LLM调用统计概览失败', e);
rethrow;
}
}
/// 获取提供商统计信息
@override
Future<List<ProviderStatistics>> getProviderStatistics({
DateTime? startTime,
DateTime? endTime,
}) async {
try {
AppLogger.d(_tag, '获取提供商统计信息: startTime=$startTime, endTime=$endTime');
final queryParams = <String, dynamic>{};
if (startTime != null) queryParams['startTime'] = startTime.toIso8601String();
if (endTime != null) queryParams['endTime'] = endTime.toIso8601String();
final response = await _apiClient.getWithParams('/admin/llm-observability/statistics/providers', queryParameters: queryParams);
Map<String, dynamic> dataMap;
if (response is Map<String, dynamic> && response.containsKey('data')) {
final d = response['data'];
if (d is Map<String, dynamic>) {
dataMap = d;
} else {
AppLogger.w(_tag, '提供商统计 data 非 Map实际为: ${d.runtimeType}');
return [];
}
} else if (response is Map<String, dynamic>) {
dataMap = response;
} else {
AppLogger.w(_tag, '提供商统计响应不是 Map实际为: ${response.runtimeType}');
return [];
}
final Map<String, num> callsByProvider = Map<String, num>.from(dataMap['callsByProvider'] ?? {});
final Map<String, num> errorsByProvider = Map<String, num>.from(dataMap['errorsByProvider'] ?? {});
final Map<String, num> avgDurationByProvider = Map<String, num>.from(dataMap['avgDurationByProvider'] ?? {});
final List<ProviderStatistics> result = [];
for (final entry in callsByProvider.entries) {
final String provider = entry.key;
final int totalCalls = entry.value.toInt();
final int failed = (errorsByProvider[provider] ?? 0).toInt();
final int successful = totalCalls - failed;
final double successRate = totalCalls == 0 ? 0.0 : successful / totalCalls * 100.0;
final double avgLatency = (avgDurationByProvider[provider] ?? 0).toDouble();
final stats = LLMStatistics(
totalCalls: totalCalls,
successfulCalls: successful,
failedCalls: failed,
successRate: successRate,
averageLatency: avgLatency,
totalTokens: 0,
);
result.add(ProviderStatistics(provider: provider, statistics: stats, models: const []));
}
// 排序:按调用次数降序
result.sort((a, b) => b.statistics.totalCalls.compareTo(a.statistics.totalCalls));
return result;
} catch (e) {
AppLogger.e(_tag, '获取提供商统计信息失败', e);
// 出错时返回空列表而不是抛出异常,避免崩溃
return [];
}
}
/// 获取模型统计信息
@override
Future<List<ModelStatistics>> getModelStatistics({
DateTime? startTime,
DateTime? endTime,
}) async {
try {
AppLogger.d(_tag, '获取模型统计信息: startTime=$startTime, endTime=$endTime');
final queryParams = <String, dynamic>{};
if (startTime != null) queryParams['startTime'] = startTime.toIso8601String();
if (endTime != null) queryParams['endTime'] = endTime.toIso8601String();
final response = await _apiClient.getWithParams('/admin/llm-observability/statistics/models', queryParameters: queryParams);
Map<String, dynamic> dataMap;
if (response is Map<String, dynamic> && response.containsKey('data')) {
final d = response['data'];
if (d is Map<String, dynamic>) {
dataMap = d;
} else {
AppLogger.w(_tag, '模型统计 data 非 Map实际为: ${d.runtimeType}');
return [];
}
} else if (response is Map<String, dynamic>) {
dataMap = response;
} else {
AppLogger.w(_tag, '模型统计响应不是 Map实际为: ${response.runtimeType}');
return [];
}
final Map<String, num> callsByModel = Map<String, num>.from(dataMap['callsByModel'] ?? {});
final Map<String, num> errorsByModel = Map<String, num>.from(dataMap['errorsByModel'] ?? {});
final Map<String, num> tokensByModel = Map<String, num>.from(dataMap['tokensByModel'] ?? {});
final List<ModelStatistics> result = [];
for (final entry in callsByModel.entries) {
final String modelName = entry.key;
final int totalCalls = entry.value.toInt();
final int failed = (errorsByModel[modelName] ?? 0).toInt();
final int successful = totalCalls - failed;
final double successRate = totalCalls == 0 ? 0.0 : successful / totalCalls * 100.0;
final int totalTokens = (tokensByModel[modelName] ?? 0).toInt();
final stats = LLMStatistics(
totalCalls: totalCalls,
successfulCalls: successful,
failedCalls: failed,
successRate: successRate,
averageLatency: 0.0,
totalTokens: totalTokens,
);
// 后端未提供 provider 归属,这里尝试从模型名前缀简单推断,否则留空
final provider = _inferProviderFromModel(modelName);
result.add(ModelStatistics(modelName: modelName, provider: provider, statistics: stats));
}
// 排序:按调用次数降序
result.sort((a, b) => b.statistics.totalCalls.compareTo(a.statistics.totalCalls));
return result;
} catch (e) {
AppLogger.e(_tag, '获取模型统计信息失败', e);
// 出错时返回空列表而不是抛出异常,避免崩溃
return [];
}
}
String _inferProviderFromModel(String modelName) {
final lower = modelName.toLowerCase();
if (lower.contains('gpt') || lower.contains('o1') || lower.contains('openai')) return 'OpenAI';
if (lower.contains('claude') || lower.contains('anthropic')) return 'Anthropic';
if (lower.contains('gemini') || lower.contains('google') || lower.contains('palm')) return 'Google';
if (lower.contains('glm') || lower.contains('zhipu')) return 'ZhipuAI';
if (lower.contains('qwen') || lower.contains('dashscope') || lower.contains('ali')) return 'AliCloud';
return '';
}
/// 获取用户统计信息
@override
Future<List<UserStatistics>> getUserStatistics({
DateTime? startTime,
DateTime? endTime,
}) async {
try {
AppLogger.d(_tag, '获取用户统计信息: startTime=$startTime, endTime=$endTime');
final queryParams = <String, dynamic>{};
if (startTime != null) queryParams['startTime'] = startTime.toIso8601String();
if (endTime != null) queryParams['endTime'] = endTime.toIso8601String();
final response = await _apiClient.getWithParams('/admin/llm-observability/statistics/users', queryParameters: queryParams);
Map<String, dynamic> dataMap;
if (response is Map<String, dynamic> && response.containsKey('data')) {
final d = response['data'];
if (d is Map<String, dynamic>) {
dataMap = d;
} else {
AppLogger.w(_tag, '用户统计 data 非 Map实际为: ${d.runtimeType}');
return [];
}
} else if (response is Map<String, dynamic>) {
dataMap = response;
} else {
AppLogger.w(_tag, '用户统计响应不是 Map实际为: ${response.runtimeType}');
return [];
}
final Map<String, num> callsByUser = Map<String, num>.from(dataMap['callsByUser'] ?? {});
final Map<String, num> tokensByUser = Map<String, num>.from(dataMap['tokensByUser'] ?? {});
final Map<String, num> errorsByUser = Map<String, num>.from(dataMap['errorsByUser'] ?? {});
final List<UserStatistics> result = [];
for (final entry in callsByUser.entries) {
final String userId = entry.key;
final int totalCalls = entry.value.toInt();
final int failed = (errorsByUser[userId] ?? 0).toInt();
final int successful = totalCalls - failed;
final double successRate = totalCalls == 0 ? 0.0 : successful / totalCalls * 100.0;
final int totalTokens = (tokensByUser[userId] ?? 0).toInt();
final stats = LLMStatistics(
totalCalls: totalCalls,
successfulCalls: successful,
failedCalls: failed,
successRate: successRate,
averageLatency: 0.0,
totalTokens: totalTokens,
);
result.add(UserStatistics(
userId: userId,
username: null,
statistics: stats,
topModels: const [],
topProviders: const [],
));
}
// 排序:按调用次数降序
result.sort((a, b) => b.statistics.totalCalls.compareTo(a.statistics.totalCalls));
return result;
} catch (e) {
AppLogger.e(_tag, '获取用户统计信息失败', e);
// 出错时返回空列表而不是抛出异常,避免崩溃
return [];
}
}
/// 获取错误统计信息
@override
Future<List<ErrorStatistics>> getErrorStatistics({
DateTime? startTime,
DateTime? endTime,
}) async {
try {
AppLogger.d(_tag, '获取错误统计信息: startTime=$startTime, endTime=$endTime');
final queryParams = <String, dynamic>{};
if (startTime != null) queryParams['startTime'] = startTime.toIso8601String();
if (endTime != null) queryParams['endTime'] = endTime.toIso8601String();
final response = await _apiClient.getWithParams('/admin/llm-observability/statistics/errors', queryParameters: queryParams);
List<dynamic> dataList;
if (response is Map<String, dynamic> && response.containsKey('data')) {
final data = response['data'];
if (data is List) {
dataList = data;
} else {
AppLogger.w(_tag, '错误统计数据不是List格式: ${data.runtimeType}');
return [];
}
} else if (response is List) {
dataList = response;
} else {
AppLogger.w(_tag, '错误统计响应格式错误,返回空列表: ${response.runtimeType}');
return [];
}
return dataList
.map((item) => ErrorStatistics.fromJson(item as Map<String, dynamic>))
.toList();
} catch (e) {
AppLogger.e(_tag, '获取错误统计信息失败', e);
return [];
}
}
/// 获取性能统计信息
@override
Future<PerformanceStatistics> getPerformanceStatistics({
DateTime? startTime,
DateTime? endTime,
}) async {
try {
AppLogger.d(_tag, '获取性能统计信息: startTime=$startTime, endTime=$endTime');
final queryParams = <String, dynamic>{};
if (startTime != null) queryParams['startTime'] = startTime.toIso8601String();
if (endTime != null) queryParams['endTime'] = endTime.toIso8601String();
final response = await _apiClient.getWithParams('/admin/llm-observability/statistics/performance', queryParameters: queryParams);
Map<String, dynamic> data;
if (response is Map<String, dynamic> && response.containsKey('data')) {
data = response['data'];
} else if (response is Map<String, dynamic>) {
data = response;
} else {
throw ApiException(-1, '性能统计响应格式错误');
}
return PerformanceStatistics.fromJson(data);
} catch (e) {
AppLogger.e(_tag, '获取性能统计信息失败', e);
// 返回空的性能统计对象
return const PerformanceStatistics(
averageLatency: 0.0,
medianLatency: 0.0,
p95Latency: 0.0,
p99Latency: 0.0,
averageThroughput: 0.0,
latencyTrends: [],
throughputTrends: [],
);
}
}
/// 获取趋势数据
@override
Future<Map<String, dynamic>> getTrends({
String? metric,
String? groupBy,
String? businessType,
String? model,
String? provider,
String interval = 'hour',
DateTime? startTime,
DateTime? endTime,
}) async {
try {
final queryParams = <String, dynamic>{
'interval': interval,
};
if (metric != null) queryParams['metric'] = metric;
if (groupBy != null) queryParams['groupBy'] = groupBy;
if (businessType != null) queryParams['businessType'] = businessType;
if (model != null) queryParams['model'] = model;
if (provider != null) queryParams['provider'] = provider;
if (startTime != null) queryParams['startTime'] = startTime.toIso8601String();
if (endTime != null) queryParams['endTime'] = endTime.toIso8601String();
final response = await _apiClient.getWithParams('/admin/llm-observability/statistics/trends', queryParameters: queryParams);
if (response is Map<String, dynamic> && response.containsKey('data')) {
return response['data'];
} else if (response is Map<String, dynamic>) {
return response;
} else {
throw ApiException(-1, '趋势数据响应格式错误');
}
} catch (e) {
AppLogger.e(_tag, '获取趋势数据失败', e);
rethrow;
}
}
// ==================== 系统管理 ====================
/// 导出LLM调用日志
@override
Future<List<LLMTrace>> exportTraces({
Map<String, dynamic>? filterCriteria,
}) async {
try {
AppLogger.d(_tag, '导出LLM调用日志: filterCriteria=$filterCriteria');
dynamic response;
try {
// 优先使用带过滤的高级导出端点
response = await _apiClient.post('/admin/llm-observability/export2', data: filterCriteria ?? {});
} catch (e) {
AppLogger.w(_tag, 'export2 不可用,回退到 export', e);
response = await _apiClient.post('/admin/llm-observability/export', data: filterCriteria ?? {});
}
if (response is Map<String, dynamic> && response.containsKey('data')) {
final List<dynamic> traces = response['data'];
return traces.map((trace) => LLMTrace.fromJson(trace)).toList();
} else if (response is List<dynamic>) {
return response.map((trace) => LLMTrace.fromJson(trace)).toList();
} else {
throw ApiException(-1, '导出日志响应格式错误');
}
} catch (e) {
AppLogger.e(_tag, '导出LLM调用日志失败', e);
rethrow;
}
}
/// 清理旧日志
@override
Future<Map<String, dynamic>> cleanupOldTraces(DateTime beforeTime) async {
try {
AppLogger.d(_tag, '清理旧日志: beforeTime=$beforeTime');
final response = await _apiClient.deleteWithParams('/admin/llm-observability/cleanup', queryParameters: {
'beforeTime': beforeTime.toIso8601String(),
});
if (response is Map<String, dynamic> && response.containsKey('data')) {
return response['data'];
} else if (response is Map<String, dynamic>) {
return response;
} else {
return {'deletedCount': 0};
}
} catch (e) {
AppLogger.e(_tag, '清理旧日志失败', e);
rethrow;
}
}
/// 获取系统健康状态
@override
Future<SystemHealthStatus> getSystemHealth() async {
try {
AppLogger.d(_tag, '获取系统健康状态');
final response = await _apiClient.getWithParams('/admin/llm-observability/health');
Map<String, dynamic> data;
if (response is Map<String, dynamic> && response.containsKey('data')) {
data = response['data'];
} else if (response is Map<String, dynamic>) {
data = response;
} else {
throw ApiException(-1, '系统健康状态响应格式错误');
}
return SystemHealthStatus.fromJson(data);
} catch (e) {
AppLogger.e(_tag, '获取系统健康状态失败', e);
// 返回默认的系统健康状态
return const SystemHealthStatus(
components: {},
status: HealthStatus.unknown,
);
}
}
/// 获取数据库状态
@override
Future<Map<String, dynamic>> getDatabaseStatus() async {
try {
AppLogger.d(_tag, '获取数据库状态');
final response = await _apiClient.getWithParams('/admin/llm-observability/database/status');
if (response is Map<String, dynamic> && response.containsKey('data')) {
return response['data'];
} else if (response is Map<String, dynamic>) {
return response;
} else {
throw ApiException(-1, '数据库状态响应格式错误');
}
} catch (e) {
AppLogger.e(_tag, '获取数据库状态失败', e);
rethrow;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,101 @@
import 'admin_repository_impl.dart';
import '../../base/api_client.dart';
import '../../base/api_exception.dart';
import '../../../../models/prompt_models.dart';
import '../../../../utils/logger.dart';
extension PromptTemplateExtraApis on AdminRepositoryImpl {
static const String _tag = 'AdminRepository(Extra)';
/// 获取待审核模板列表
Future<List<PromptTemplate>> getPendingTemplates() async {
try {
AppLogger.d(_tag, '🔍 获取待审核模板列表');
final api = ApiClient();
final response = await api.get('/admin/prompt-templates/pending');
final data = (response is Map<String, dynamic>) ? (response['data'] ?? response) : response;
if (data is List) {
AppLogger.d(_tag, '✅ 获取待审核模板列表成功: count=${data.length}');
return data.map((json) => PromptTemplate.fromJson(json as Map<String, dynamic>)).toList();
}
throw ApiException(-1, '待审核模板列表响应格式错误');
} catch (e) {
AppLogger.e(_tag, '❌ 获取待审核模板列表失败', e);
rethrow;
}
}
/// 获取官方认证模板列表
Future<List<PromptTemplate>> getVerifiedTemplates() async {
try {
AppLogger.d(_tag, '🔍 获取官方认证模板列表');
final api = ApiClient();
final response = await api.get('/admin/prompt-templates/verified');
final data = (response is Map<String, dynamic>) ? (response['data'] ?? response) : response;
if (data is List) {
AppLogger.d(_tag, '✅ 获取官方认证模板列表成功: count=${data.length}');
return data.map((json) => PromptTemplate.fromJson(json as Map<String, dynamic>)).toList();
}
throw ApiException(-1, '官方认证模板列表响应格式错误');
} catch (e) {
AppLogger.e(_tag, '❌ 获取官方认证模板列表失败', e);
rethrow;
}
}
/// 获取所有用户模板列表(包括私有和公共)
Future<List<PromptTemplate>> getAllUserTemplates({
int page = 0,
int size = 20,
String? search,
}) async {
try {
AppLogger.d(_tag, '🔍 获取所有用户模板列表: page=$page, size=$size, search=$search');
String path = '/admin/prompt-templates/all-user?page=$page&size=$size';
if (search != null && search.isNotEmpty) {
path += '&search=${Uri.encodeComponent(search)}';
}
final api = ApiClient();
final response = await api.get(path);
final data = (response is Map<String, dynamic>) ? (response['data'] ?? response) : response;
if (data is List) {
AppLogger.d(_tag, '✅ 获取所有用户模板列表成功: count=${data.length}');
return data.map((json) => PromptTemplate.fromJson(json as Map<String, dynamic>)).toList();
} else if (data is Map<String, dynamic> && data.containsKey('content')) {
// 处理分页响应
final content = data['content'] as List;
AppLogger.d(_tag, '✅ 获取所有用户模板列表成功(分页): count=${content.length}');
return content.map((json) => PromptTemplate.fromJson(json as Map<String, dynamic>)).toList();
}
throw ApiException(-1, '所有用户模板列表响应格式错误');
} catch (e) {
AppLogger.e(_tag, '❌ 获取所有用户模板列表失败', e);
rethrow;
}
}
/// 更新模板
Future<PromptTemplate> updateTemplate(String templateId, PromptTemplate template) async {
try {
AppLogger.d(_tag, '🔄 更新模板: templateId=$templateId, name=${template.name}');
final api = ApiClient();
final response = await api.put('/admin/prompt-templates/$templateId', data: template.toJson());
final data = (response is Map<String, dynamic>) ? (response['data'] ?? response) : response;
if (data is Map<String, dynamic>) {
AppLogger.d(_tag, '✅ 更新模板成功: ${template.name}');
return PromptTemplate.fromJson(data);
}
throw ApiException(-1, '更新模板响应格式错误');
} catch (e) {
AppLogger.e(_tag, '❌ 更新模板失败', e);
rethrow;
}
}
}

View File

@@ -0,0 +1,567 @@
import 'package:ainoval/models/preset_models.dart';
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/services/api_service/base/api_exception.dart';
import 'package:ainoval/services/api_service/repositories/ai_preset_repository.dart';
import 'package:ainoval/utils/logger.dart';
/// AI预设仓储实现类
class AIPresetRepositoryImpl implements AIPresetRepository {
final ApiClient apiClient;
final String _tag = 'AIPresetRepository';
AIPresetRepositoryImpl({required this.apiClient});
// 🚀 新增:统一解包 ApiResponse.data
dynamic _extractData(dynamic response) {
if (response is Map<String, dynamic> && response.containsKey('data')) {
return response['data'];
}
return response;
}
@override
Future<AIPromptPreset> createPreset(CreatePresetRequest request) async {
try {
AppLogger.d(_tag, '🔍 创建AI预设: ${request.presetName}');
// 🚀 调用新的AIPromptPresetController接口
final response = await apiClient.post(
'/ai/presets',
data: request.toJson(),
);
// 🚀 处理ApiResponse包装格式
final data = _extractData(response);
final preset = AIPromptPreset.fromJson(data);
AppLogger.i(_tag, '📘 预设创建成功: ${preset.presetId}');
return preset;
} catch (e) {
AppLogger.e(_tag, '❌ 创建预设失败', e);
rethrow;
}
}
@override
Future<List<AIPromptPreset>> getUserPresets({String? userId, String featureType = 'AI_CHAT'}) async {
try {
AppLogger.d(_tag, '获取用户预设列表: userId=$userId, featureType=$featureType');
String path = '/ai/presets';
final List<String> query = [];
// 必填参数 featureType
query.add('featureType=${Uri.encodeComponent(featureType)}');
// 可选 userId
if (userId != null) {
query.add('userId=$userId');
}
if (query.isNotEmpty) {
path = '$path?${query.join('&')}';
}
final response = await apiClient.get(path);
final data = _extractData(response);
if (data is! List) {
throw ApiException(-1, '响应格式不正确期望List类型');
}
final presets = data.map((json) => AIPromptPreset.fromJson(json)).toList();
AppLogger.i(_tag, '获取到 ${presets.length} 个用户预设');
return presets;
} catch (e) {
AppLogger.e(_tag, '获取用户预设列表失败', e);
rethrow;
}
}
@override
Future<List<AIPromptPreset>> searchPresets(PresetSearchParams params) async {
try {
AppLogger.d(_tag, '搜索预设: ${params.keyword}');
final queryParams = params.toQueryParams();
String path = '/ai/presets/search';
if (queryParams.isNotEmpty) {
final queryString = queryParams.entries
.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value.toString())}')
.join('&');
path = '$path?$queryString';
}
final response = await apiClient.get(path);
final data = _extractData(response);
if (data is! List) {
throw ApiException(-1, '响应格式不正确期望List类型');
}
final presets = data.map((json) => AIPromptPreset.fromJson(json)).toList();
AppLogger.i(_tag, '搜索到 ${presets.length} 个预设');
return presets;
} catch (e) {
AppLogger.e(_tag, '搜索预设失败', e);
rethrow;
}
}
@override
Future<AIPromptPreset> getPresetById(String presetId) async {
try {
AppLogger.d(_tag, '获取预设详情: $presetId');
final response = await apiClient.get('/ai/presets/detail/$presetId');
final data = _extractData(response);
final preset = AIPromptPreset.fromJson(data);
AppLogger.i(_tag, '获取预设详情成功: ${preset.presetName}');
return preset;
} catch (e) {
AppLogger.e(_tag, '获取预设详情失败: $presetId', e);
rethrow;
}
}
@override
Future<AIPromptPreset> overwritePreset(AIPromptPreset preset) async {
try {
AppLogger.d(_tag, '覆盖更新预设: ${preset.presetId}');
final response = await apiClient.put(
'/ai/presets/${preset.presetId}',
data: preset.toJson(),
);
final data = _extractData(response);
final updatedPreset = AIPromptPreset.fromJson(data);
AppLogger.i(_tag, '预设覆盖更新成功: ${updatedPreset.presetName}');
return updatedPreset;
} catch (e) {
AppLogger.e(_tag, '覆盖更新预设失败: ${preset.presetId}', e);
rethrow;
}
}
@override
Future<AIPromptPreset> updatePresetInfo(String presetId, UpdatePresetInfoRequest request) async {
try {
AppLogger.d(_tag, '更新预设信息: $presetId');
final response = await apiClient.put(
'/ai/presets/$presetId/info',
data: request.toJson(),
);
final data = _extractData(response);
final preset = AIPromptPreset.fromJson(data);
AppLogger.i(_tag, '预设信息更新成功: ${preset.presetName}');
return preset;
} catch (e) {
AppLogger.e(_tag, '更新预设信息失败: $presetId', e);
rethrow;
}
}
@override
Future<AIPromptPreset> updatePresetPrompts(String presetId, UpdatePresetPromptsRequest request) async {
try {
AppLogger.d(_tag, '更新预设提示词: $presetId');
final response = await apiClient.put(
'/ai/presets/$presetId/prompts',
data: request.toJson(),
);
final data = _extractData(response);
final preset = AIPromptPreset.fromJson(data);
AppLogger.i(_tag, '预设提示词更新成功');
return preset;
} catch (e) {
AppLogger.e(_tag, '更新预设提示词失败: $presetId', e);
rethrow;
}
}
@override
Future<void> deletePreset(String presetId) async {
try {
AppLogger.d(_tag, '删除预设: $presetId');
await apiClient.delete('/ai/presets/$presetId');
AppLogger.i(_tag, '预设删除成功: $presetId');
} catch (e) {
AppLogger.e(_tag, '删除预设失败: $presetId', e);
rethrow;
}
}
@override
Future<AIPromptPreset> duplicatePreset(String presetId, DuplicatePresetRequest request) async {
try {
AppLogger.d(_tag, '复制预设: $presetId -> ${request.newPresetName}');
final response = await apiClient.post(
'/ai/presets/$presetId/duplicate',
data: request.toJson(),
);
final data = _extractData(response);
final preset = AIPromptPreset.fromJson(data);
AppLogger.i(_tag, '预设复制成功: ${preset.presetId}');
return preset;
} catch (e) {
AppLogger.e(_tag, '复制预设失败: $presetId', e);
rethrow;
}
}
@override
Future<AIPromptPreset> toggleFavorite(String presetId) async {
try {
AppLogger.d(_tag, '切换预设收藏状态: $presetId');
final response = await apiClient.post('/ai/presets/$presetId/favorite');
final data = _extractData(response);
final preset = AIPromptPreset.fromJson(data);
AppLogger.i(_tag, '预设收藏状态切换成功: ${preset.isFavorite ? "已收藏" : "已取消收藏"}');
return preset;
} catch (e) {
AppLogger.e(_tag, '切换预设收藏状态失败: $presetId', e);
rethrow;
}
}
@override
Future<void> recordPresetUsage(String presetId) async {
try {
AppLogger.d(_tag, '记录预设使用: $presetId');
await apiClient.post('/ai/presets/$presetId/usage');
AppLogger.v(_tag, '预设使用记录成功: $presetId');
} catch (e) {
AppLogger.w(_tag, '记录预设使用失败: $presetId', e);
// 使用记录失败不抛出异常,不影响主要流程
}
}
@override
Future<PresetStatistics> getPresetStatistics() async {
try {
AppLogger.d(_tag, '获取预设统计信息');
final response = await apiClient.get('/ai/presets/statistics');
final data = _extractData(response);
final statistics = PresetStatistics.fromJson(data);
AppLogger.i(_tag, '获取预设统计信息成功: 总数 ${statistics.totalPresets}');
return statistics;
} catch (e) {
AppLogger.e(_tag, '获取预设统计信息失败', e);
rethrow;
}
}
@override
Future<List<AIPromptPreset>> getFavoritePresets({String? novelId, String? featureType}) async {
try {
AppLogger.d(_tag, '获取收藏预设列表: novelId=$novelId, featureType=$featureType');
String path = '/ai/presets/favorites';
List<String> queryParams = [];
if (novelId != null) {
queryParams.add('novelId=$novelId');
}
if (featureType != null) {
queryParams.add('featureType=$featureType');
}
if (queryParams.isNotEmpty) {
path = '$path?${queryParams.join('&')}';
}
final response = await apiClient.get(path);
final data = _extractData(response);
if (data is! List) {
throw ApiException(-1, '响应格式不正确期望List类型');
}
final presets = data.map((json) => AIPromptPreset.fromJson(json)).toList();
AppLogger.i(_tag, '获取到 ${presets.length} 个收藏预设');
return presets;
} catch (e) {
AppLogger.e(_tag, '获取收藏预设列表失败', e);
rethrow;
}
}
@override
Future<List<AIPromptPreset>> getRecentlyUsedPresets({int limit = 10, String? novelId, String? featureType}) async {
try {
AppLogger.d(_tag, '获取最近使用预设列表: 限制 $limit, novelId=$novelId, featureType=$featureType');
List<String> queryParams = ['limit=$limit'];
if (novelId != null) {
queryParams.add('novelId=$novelId');
}
if (featureType != null) {
queryParams.add('featureType=$featureType');
}
String path = '/ai/presets/recent?${queryParams.join('&')}';
final response = await apiClient.get(path);
final data = _extractData(response);
if (data is! List) {
throw ApiException(-1, '响应格式不正确期望List类型');
}
final presets = data.map((json) => AIPromptPreset.fromJson(json)).toList();
AppLogger.i(_tag, '获取到 ${presets.length} 个最近使用预设');
return presets;
} catch (e) {
AppLogger.e(_tag, '获取最近使用预设列表失败', e);
rethrow;
}
}
@override
Future<List<AIPromptPreset>> getPresetsByFeatureType(String featureType) async {
try {
AppLogger.d(_tag, '获取指定功能类型预设: $featureType');
final response = await apiClient.get(
'/ai/presets/feature/$featureType',
);
final data = _extractData(response);
if (data is! List) {
throw ApiException(-1, '响应格式不正确期望List类型');
}
final presets = data.map((json) => AIPromptPreset.fromJson(json)).toList();
AppLogger.i(_tag, '获取到 ${presets.length}$featureType 类型预设');
return presets;
} catch (e) {
AppLogger.e(_tag, '获取指定功能类型预设失败: $featureType', e);
rethrow;
}
}
// ============ 新增:系统预设管理接口实现 ============
@override
Future<List<AIPromptPreset>> getSystemPresets({String? featureType}) async {
try {
AppLogger.d(_tag, '获取系统预设列表: featureType=$featureType');
String path = '/ai/presets/system';
if (featureType != null) {
path = '$path?featureType=$featureType';
}
final response = await apiClient.get(path);
final data = _extractData(response);
if (data is! List) {
throw ApiException(-1, '响应格式不正确期望List类型');
}
final presets = data.map((json) => AIPromptPreset.fromJson(json)).toList();
AppLogger.i(_tag, '获取到 ${presets.length} 个系统预设');
return presets;
} catch (e) {
AppLogger.e(_tag, '获取系统预设列表失败', e);
rethrow;
}
}
@override
Future<List<AIPromptPreset>> getQuickAccessPresets({String? featureType, String? novelId}) async {
try {
AppLogger.d(_tag, '获取快捷访问预设: featureType=$featureType, novelId=$novelId');
String path = '/ai/presets/quick-access';
List<String> queryParams = [];
if (featureType != null) {
queryParams.add('featureType=$featureType');
}
if (novelId != null) {
queryParams.add('novelId=$novelId');
}
if (queryParams.isNotEmpty) {
path = '$path?${queryParams.join('&')}';
}
final response = await apiClient.get(path);
final data = _extractData(response);
if (data is! List) {
throw ApiException(-1, '响应格式不正确期望List类型');
}
final presets = data.map((json) => AIPromptPreset.fromJson(json)).toList();
AppLogger.i(_tag, '获取到 ${presets.length} 个快捷访问预设');
return presets;
} catch (e) {
AppLogger.e(_tag, '获取快捷访问预设失败', e);
rethrow;
}
}
@override
Future<AIPromptPreset> toggleQuickAccess(String presetId) async {
try {
AppLogger.d(_tag, '切换预设快捷访问状态: $presetId');
final response = await apiClient.post('/ai/presets/$presetId/quick-access');
final data = _extractData(response);
final preset = AIPromptPreset.fromJson(data);
AppLogger.i(_tag, '预设快捷访问状态切换成功: ${preset.showInQuickAccess ? "已加入快捷访问" : "已移出快捷访问"}');
return preset;
} catch (e) {
AppLogger.e(_tag, '切换预设快捷访问状态失败: $presetId', e);
rethrow;
}
}
@override
Future<List<AIPromptPreset>> getPresetsByIds(List<String> presetIds) async {
try {
AppLogger.d(_tag, '批量获取预设: ${presetIds.length}');
final response = await apiClient.post(
'/ai/presets/batch',
data: {'presetIds': presetIds},
);
final data = _extractData(response);
if (data is! List) {
throw ApiException(-1, '响应格式不正确期望List类型');
}
final presets = data.map((json) => AIPromptPreset.fromJson(json)).toList();
AppLogger.i(_tag, '批量获取到 ${presets.length} 个预设');
return presets;
} catch (e) {
AppLogger.e(_tag, '批量获取预设失败', e);
rethrow;
}
}
@override
Future<Map<String, List<AIPromptPreset>>> getUserPresetsByFeatureType({String? userId}) async {
try {
AppLogger.d(_tag, '获取用户预设按功能类型分组: userId=$userId');
String path = '/ai/presets/grouped';
if (userId != null) {
path = '$path?userId=$userId';
}
final response = await apiClient.get(path);
final data = _extractData(response);
if (data is! Map<String, dynamic>) {
throw ApiException(-1, '响应格式不正确期望Map类型');
}
final Map<String, List<AIPromptPreset>> groupedPresets = {};
data.forEach((featureType, presetsJson) {
try {
if (presetsJson is List) {
final presets = presetsJson.map((json) => AIPromptPreset.fromJson(json)).toList();
groupedPresets[featureType] = presets;
}
} catch (e) {
AppLogger.w(_tag, '解析功能类型预设失败: $featureType', e);
}
});
AppLogger.i(_tag, '获取到 ${groupedPresets.length} 个功能类型的分组预设');
return groupedPresets;
} catch (e) {
AppLogger.e(_tag, '获取用户预设按功能类型分组失败', e);
rethrow;
}
}
@override
Future<Map<String, dynamic>> getFeatureTypePresetManagement(String featureType, {String? novelId}) async {
try {
AppLogger.d(_tag, '获取功能类型预设管理信息: featureType=$featureType, novelId=$novelId');
String path = '/ai/presets/management/$featureType';
if (novelId != null) {
path = '$path?novelId=$novelId';
}
final response = await apiClient.get(path);
final data = _extractData(response);
if (data is! Map<String, dynamic>) {
throw ApiException(-1, '响应格式不正确期望Map类型');
}
AppLogger.i(_tag, '获取功能类型预设管理信息成功: $featureType');
return data;
} catch (e) {
AppLogger.e(_tag, '获取功能类型预设管理信息失败: $featureType', e);
rethrow;
}
}
@override
Future<PresetListResponse> getFeaturePresetList(String featureType, {String? novelId}) async {
try {
AppLogger.d(_tag, '获取功能预设列表: featureType=$featureType, novelId=$novelId');
Map<String, String> queryParams = {
'featureType': featureType,
};
if (novelId != null) {
queryParams['novelId'] = novelId;
}
final response = await apiClient.get(
'/ai/presets/feature-list?${queryParams.entries.map((e) => '${e.key}=${e.value}').join('&')}',
);
final data = _extractData(response);
if (data is! Map<String, dynamic>) {
throw ApiException(-1, '响应格式不正确期望Map类型');
}
final presetListResponse = PresetListResponse.fromJson(data);
AppLogger.i(_tag, '获取功能预设列表成功: 收藏${presetListResponse.favorites.length}个, '
'最近使用${presetListResponse.recentUsed.length}个, '
'推荐${presetListResponse.recommended.length}');
return presetListResponse;
} catch (e) {
AppLogger.e(_tag, '获取功能预设列表失败: $featureType', e);
rethrow;
}
}
}

View File

@@ -0,0 +1,437 @@
import 'dart:typed_data';
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/services/api_service/base/api_exception.dart';
import 'package:ainoval/services/api_service/repositories/storage_repository.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:flutter_oss_aliyun/flutter_oss_aliyun.dart';
import 'package:mime/mime.dart';
import 'package:dio/dio.dart';
import 'package:http_parser/http_parser.dart';
/// 阿里云OSS存储仓库实现使用 POST Policy 上传
class AliyunOssStorageRepository implements StorageRepository {
final ApiClient _apiClient;
Client? _ossClient;
final Dio _dio = Dio();
Map<String, dynamic>? _lastCredential;
AliyunOssStorageRepository(this._apiClient);
@override
Future<Map<String, dynamic>> getCoverUploadCredential({
required String novelId,
required String fileName,
String? contentType,
}) async {
try {
// 参数校验
if (novelId.isEmpty) {
throw ApiException(-1, '小说ID不能为空');
}
// 使用默认文件名
final String safeFileName = fileName.isEmpty ? 'cover.jpg' : fileName;
// 获取MIME类型如果未提供
final String mimeType = contentType ?? _getMimeType(safeFileName);
// 调用后端API获取 POST Policy 上传凭证
final credential = await _apiClient.getCoverUploadCredential(novelId);
// 校验返回的凭证是否包含 POST Policy 必要字段
final requiredFields = ['accessKeyId', 'policy', 'signature', 'key', 'host'];
final missingFields = requiredFields.where((field) =>
!credential.containsKey(field) ||
credential[field] == null ||
credential[field].toString().isEmpty).toList();
if (missingFields.isNotEmpty) {
throw ApiException(
-1, '获取上传凭证失败:缺少必要字段 ${missingFields.join(', ')}');
}
// 存储凭证,如果需要重新初始化客户端时使用
_lastCredential = Map<String, dynamic>.from(credential);
// 添加前端需要的额外信息
credential['fileName'] = safeFileName;
credential['contentType'] = mimeType;
AppLogger.d(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'获取 POST Policy 上传凭证成功:${credential.keys.join(', ')}',
);
return credential;
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'获取上传凭证失败',
e,
(e is DioException) ? e.stackTrace : StackTrace.current,
);
if (e is ApiException) {
rethrow;
}
throw ApiException(-1, '获取上传凭证失败: $e');
}
}
@override
Future<String> uploadCoverImage({
required String novelId,
required Uint8List fileBytes,
required String fileName,
String? contentType,
bool updateNovelCover = true,
}) async {
try {
// 参数校验
if (fileBytes.isEmpty) throw ApiException(-1, '上传内容为空');
final safeFileName = fileName.isEmpty ? 'cover.jpg' : fileName;
final mimeType = contentType ?? _getMimeType(safeFileName);
// 获取 POST Policy 上传凭证
final credential = await getCoverUploadCredential(
novelId: novelId,
fileName: safeFileName,
contentType: mimeType,
);
AppLogger.d(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'准备使用 POST Policy 上传,凭证字段: ${credential.keys.join(', ')}',
);
// 从凭证中提取必要字段
final String key = credential['key'].toString();
final String policy = credential['policy'].toString();
final String accessKeyId = credential['accessKeyId'].toString(); // Should be 'OSSAccessKeyId' in form
final String signature = credential['signature'].toString();
final String host = credential['host'].toString(); // Upload URL
// final String? callback = credential['callback']?.toString(); // Optional callback
// 准备 FormData
final formData = FormData.fromMap({
'key': key,
'policy': policy,
'OSSAccessKeyId': accessKeyId, // Field name expected by OSS
'signature': signature,
'success_action_status': '200', // Or '204' - request success status
'Content-Type': mimeType, // Explicitly set Content-Type for the request part
'file': MultipartFile.fromBytes(
fileBytes,
filename: safeFileName, // Use the safe file name
contentType: MediaType.parse(mimeType), // Use MediaType for content type
),
// if (callback != null) 'callback': callback, // Add callback if present
});
AppLogger.d(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'开始 POST Policy 上传文件: host=$host, key=$key, size=${fileBytes.length}, contentType=$mimeType',
);
try {
// 使用 Dio 发送 POST 请求
final response = await _dio.post(
host,
data: formData,
onSendProgress: (count, total) {
// Optional: Handle progress update
AppLogger.d(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'上传进度: $count/$total',
);
},
options: Options(
// OSS might return 200 or 204 on success depending on configuration
// Dio considers only 2xx as success by default.
// We check manually below.
followRedirects: false,
validateStatus: (status) {
return status != null; // Accept any status code, validate below
},
),
);
// 检查响应状态
if (response.statusCode != 200 && response.statusCode != 204) {
String errorBody = response.data?.toString() ?? 'No response body';
// OSS often returns XML errors for POST uploads
if (errorBody.contains('<Code>') && errorBody.contains('<Message>')) {
// Try to extract OSS error details
final codeMatch = RegExp(r'<Code>(.*?)<\/Code>').firstMatch(errorBody);
final messageMatch = RegExp(r'<Message>(.*?)<\/Message>').firstMatch(errorBody);
errorBody = 'OSS Error: Code=${codeMatch?.group(1) ?? 'Unknown'}, Message=${messageMatch?.group(1) ?? 'Unknown'}';
}
AppLogger.e(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'OSS POST Policy 上传失败,状态码: ${response.statusCode}, 消息: ${response.statusMessage ?? errorBody}',
Exception('OSS POST Policy Upload Failed'),
StackTrace.current,
);
throw ApiException(response.statusCode ?? -1, '上传失败: ${response.statusMessage ?? errorBody}');
}
AppLogger.i(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'POST Policy 上传成功',
);
// 构建文件URL (This might need adjustment depending on 'host' format)
// If host is like 'https://bucket.endpoint', URL is host + / + key
// If host is like 'https://endpoint' and bucket is separate, adjust accordingly.
// Assuming host is the base URL for the object.
final String fileUrl = '$host/$key'; // Simplistic assumption, adjust if needed
AppLogger.i(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'上传完成文件URL: $fileUrl',
);
if (updateNovelCover) {
await _apiClient.updateNovelCover(novelId, fileUrl);
}
return fileUrl;
} on DioException catch (e) {
String errorDetails = e.message ?? e.toString();
if (e.response != null) {
errorDetails += "\nResponse Status: ${e.response?.statusCode}";
errorDetails += "\nResponse Data: ${e.response?.data}";
}
AppLogger.e(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'上传过程中发生网络或服务器错误: $errorDetails', // FIX LINTER: Merge details into message
e, // 记录原始异常
e.stackTrace, // Pass the stack trace
);
throw ApiException(e.response?.statusCode ?? -1, '上传失败: $errorDetails');
} catch (e, s) {
// 处理其他类型的异常
AppLogger.e(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'上传过程中发生未知错误',
e,
s,
);
throw ApiException(-1, '上传失败: ${e.toString()}');
}
} catch (e, s) {
AppLogger.e(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'上传封面图片失败 (外部捕获)',
e,
s,
);
if (e is ApiException) {
rethrow;
}
throw ApiException(-1, '上传封面图片失败: $e');
}
}
@override
Future<String> getFileAccessUrl({
required String fileKey,
int? expirationSeconds,
}) async {
if (_ossClient == null && _lastCredential != null) {
try {
_initOssClient(_lastCredential!);
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'Failed to re-initialize OSS client for getFileAccessUrl',
e,
StackTrace.current
);
// Fallback: Return non-signed URL if client init fails
return _buildFileUrlFromKey(fileKey);
}
}
if (_ossClient == null) {
AppLogger.w(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'OSS Client not initialized for getFileAccessUrl, returning non-signed URL.',
);
// Fallback: Return non-signed URL if client is not initialized
return _buildFileUrlFromKey(fileKey);
}
try {
// This assumes _ossClient was initialized correctly with STS creds
final url = await _ossClient!.getSignedUrl(
fileKey
);
return url;
} catch (e, s) {
AppLogger.e(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'获取文件访问URL失败 (getSignedUrl)',
e,
s,
);
// Fallback: Return non-signed URL on error
return _buildFileUrlFromKey(fileKey);
}
}
@override
Future<bool> hasValidStorageConfig() async {
try {
// Test by attempting to get credentials for a dummy file
await getCoverUploadCredential(
novelId: 'test_config', // Use a distinct ID for testing
fileName: 'test.jpg',
);
// We assume if credentials are fetched, the config is likely valid enough
// A full test would involve a small test upload.
return true;
} catch (e) {
AppLogger.w(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'hasValidStorageConfig check failed',
e
);
return false;
}
}
/// 初始化或更新OSS客户端实例
void _initOssClient(Map<String, dynamic> credential) {
try {
// 从凭证中提取 STS 或 AK/SK 信息
final accessKeyId = credential['accessKeyId']?.toString();
final accessKeySecret = credential['accessKeySecret']?.toString();
final securityToken = credential['securityToken']?.toString(); // STS Token
final endpoint = credential['endpoint']?.toString();
final bucketName = credential['bucket']?.toString();
final expiration = credential['expiration']?.toString(); // STS凭证过期时间 (ISO 8601)
// 校验必要参数
if (accessKeyId == null || accessKeyId.isEmpty ||
accessKeySecret == null || accessKeySecret.isEmpty ||
// securityToken 对于 STS 是必需的
(securityToken == null || securityToken.isEmpty) ||
endpoint == null || endpoint.isEmpty ||
bucketName == null || bucketName.isEmpty) {
throw ApiException(-1, 'OSS客户端初始化失败凭证缺少必要参数 (Id, Secret, Token, Endpoint, Bucket)');
}
// 检查凭证是否已过期 (可选但推荐)
DateTime? expireTime;
if (expiration != null) {
try {
expireTime = DateTime.parse(expiration).toUtc();
// 留一些缓冲时间比如提前5分钟认为过期
if (DateTime.now().toUtc().isAfter(expireTime.subtract(const Duration(minutes: 5)))) {
AppLogger.w('Services/api_service/repositories/impl/aliyun_oss_storage_repository', 'STS凭证即将或已经过期建议重新获取');
// 这里可以决定是否强制重新获取凭证,或者让后续操作失败
}
} catch(e) {
AppLogger.w('Services/api_service/repositories/impl/aliyun_oss_storage_repository', '解析凭证过期时间失败: $expiration', e);
}
}
AppLogger.d(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'初始化OSS客户端: endpoint=$endpoint, bucket=$bucketName, 使用STS凭证',
);
// 使用 STS 凭证初始化 Client
// 确保 flutter_oss_aliyun 支持直接传入 STS token
_ossClient = Client.init(
// region: credential['region']?.toString(), // 如果需要指定 region
ossEndpoint: endpoint, // 使用后端提供的 endpoint
bucketName: bucketName, // 使用后端提供的 bucket
// signVersion: SignVersion.V4, // 显式指定V4 (如果SDK支持)
authGetter: () => Auth(
accessKey: accessKeyId,
accessSecret: accessKeySecret,
secureToken: securityToken, // 传递 STS Token
expire: expiration ?? DateTime.now().add(const Duration(hours: 1)).toIso8601String(),
),
);
AppLogger.i(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'OSS客户端初始化成功 (使用STS凭证)',
);
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'初始化OSS客户端失败',
e,
);
_ossClient = null; // 初始化失败,清空客户端
if (e is ApiException) rethrow;
throw ApiException(-1, '初始化OSS客户端失败: $e');
}
}
/// 根据凭证构建文件URL
String _buildFileUrl(Map<String, dynamic> credential) {
// 优先使用后端可能直接提供的 fileUrl 字段 (如果后端逻辑包含)
if (credential.containsKey('fileUrl') && credential['fileUrl'] != null && credential['fileUrl'].toString().isNotEmpty) {
return credential['fileUrl'].toString();
}
// 从 endpoint, bucket, key 构建标准 OSS URL
final endpoint = credential['endpoint']?.toString() ?? '';
final bucket = credential['bucket']?.toString() ?? '';
final key = credential['key']?.toString() ?? '';
if (endpoint.isEmpty || bucket.isEmpty || key.isEmpty) {
AppLogger.w('Services/api_service/repositories/impl/aliyun_oss_storage_repository', '无法构建文件URL缺少 endpoint, bucket 或 key');
return 'error_url_build_failed'; // 返回错误标识或抛出异常
}
// 确保 endpoint 不包含协议头,并移除末尾斜杠
String cleanEndpoint = endpoint.replaceAll(RegExp(r'^https?://'), '');
if (cleanEndpoint.endsWith('/')) {
cleanEndpoint = cleanEndpoint.substring(0, cleanEndpoint.length - 1);
}
// 确保 key 不以斜杠开头
String cleanKey = key;
if (cleanKey.startsWith('/')) {
cleanKey = cleanKey.substring(1);
}
// 构建 URL: https://bucket.endpoint/key
return 'https://$bucket.$cleanEndpoint/$cleanKey';
}
/// Builds a potentially non-signed URL just from the key
/// Requires _lastCredential to have endpoint/bucket info.
String _buildFileUrlFromKey(String key) {
final endpoint = _lastCredential?['endpoint']?.toString();
final bucket = _lastCredential?['bucket']?.toString(); // Bucket might not be in POST creds
if (endpoint == null || endpoint.isEmpty || bucket == null || bucket.isEmpty) {
AppLogger.w('Services/api_service/repositories/impl/aliyun_oss_storage_repository',
'Cannot build file URL from key, missing endpoint/bucket in last credential');
return key; // Return key as fallback
}
String cleanEndpoint = endpoint.replaceAll(RegExp(r'^https?://'), '');
if (cleanEndpoint.endsWith('/')) {
cleanEndpoint = cleanEndpoint.substring(0, cleanEndpoint.length - 1);
}
String cleanKey = key;
if (cleanKey.startsWith('/')) {
cleanKey = cleanKey.substring(1);
}
return 'https://$bucket.$cleanEndpoint/$cleanKey';
}
/// 根据文件名获取MIME类型
String _getMimeType(String fileName) {
final mimeType = lookupMimeType(fileName);
return mimeType ?? 'application/octet-stream';
}
}

View File

@@ -0,0 +1,454 @@
import 'package:ainoval/models/analytics_data.dart';
import 'package:ainoval/models/prompt_models.dart';
import 'package:ainoval/services/api_service/repositories/analytics_repository.dart';
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/utils/date_time_parser.dart';
class AnalyticsRepositoryImpl implements AnalyticsRepository {
final ApiClient _apiClient = ApiClient();
@override
Future<AnalyticsData> getAnalyticsOverview() async {
try {
final response = await _apiClient.get('/analytics/overview');
final data = response['data'] as Map<String, dynamic>;
final rawMostPopular = data['mostPopularFunction']?.toString() ?? '';
return AnalyticsData(
totalWords: data['totalWords'] ?? 0,
monthlyNewWords: data['monthlyNewWords'] ?? 0,
totalTokens: data['totalTokens'] ?? 0,
monthlyNewTokens: data['monthlyNewTokens'] ?? 0,
functionUsageCount: (data['functionUsageCount'] ?? 0).toInt(),
mostPopularFunction: _mapFunctionToDisplay(rawMostPopular).isEmpty
? '智能续写'
: _mapFunctionToDisplay(rawMostPopular),
writingDays: (data['writingDays'] ?? 0).toInt(),
consecutiveDays: (data['consecutiveDays'] ?? 0).toInt(),
);
} catch (e) {
throw Exception('Failed to load analytics overview: $e');
}
}
@override
Future<List<TokenUsageData>> getTokenUsageTrend({
AnalyticsViewMode viewMode = AnalyticsViewMode.monthly,
DateTime? startDate,
DateTime? endDate,
}) async {
try {
final Map<String, dynamic> params = {
'viewMode': _getViewModeString(viewMode),
};
if (startDate != null) {
params['startDate'] = startDate.toIso8601String();
}
if (endDate != null) {
params['endDate'] = endDate.toIso8601String();
}
final response = await _apiClient.getWithParams('/analytics/token-usage-trend', queryParameters: params);
final List<dynamic> data = response['data'] as List<dynamic>;
final List<TokenUsageData> rawSeries = data.map((item) => TokenUsageData(
date: item['date'] as String,
inputTokens: item['inputTokens'] ?? 0,
outputTokens: item['outputTokens'] ?? 0,
totalTokens: (item['inputTokens'] ?? 0) + (item['outputTokens'] ?? 0),
modelTokens: <String, int>{}, // 后端暂不返回按模型分组的数据
)).toList();
// 补齐缺失日期,避免仅单点数据显示突兀
return _postProcessTokenSeries(
rawSeries: rawSeries,
viewMode: viewMode,
startDate: startDate,
endDate: endDate,
);
} catch (e) {
throw Exception('Failed to load token usage trend: $e');
}
}
@override
Future<List<FunctionUsageData>> getFunctionUsageStats({
required AnalyticsViewMode viewMode,
DateTime? startDate,
DateTime? endDate,
}) async {
try {
final Map<String, dynamic> params = {
'viewMode': _getViewModeString(viewMode),
};
final response = await _apiClient.getWithParams('/analytics/function-usage-stats', queryParameters: params);
final List<dynamic> data = response['data'] as List<dynamic>;
return data.map((item) => FunctionUsageData(
name: _mapFunctionToDisplay(item['function']?.toString() ?? ''),
value: (item['count'] ?? 0).toInt(),
growth: 0.0, // 后端暂不返回增长率数据
)).toList();
} catch (e) {
throw Exception('Failed to load function usage stats: $e');
}
}
@override
Future<List<ModelUsageData>> getModelUsageStats({
required AnalyticsViewMode viewMode,
DateTime? startDate,
DateTime? endDate,
}) async {
try {
final Map<String, dynamic> params = {
'viewMode': _getViewModeString(viewMode),
};
final response = await _apiClient.getWithParams('/analytics/model-usage-stats', queryParameters: params);
final List<dynamic> data = response['data'] as List<dynamic>;
return data.map((item) => ModelUsageData(
modelName: item['model'] as String,
percentage: ((item['percentage'] ?? 0.0).toDouble()).round(),
totalTokens: item['count'] ?? 0, // 使用count作为总tokens的代表
color: _getModelColor(item['model'] as String),
)).toList();
} catch (e) {
throw Exception('Failed to load model usage stats: $e');
}
}
@override
Future<List<TokenUsageRecord>> getTokenUsageRecords({
int limit = 20,
int offset = 0,
}) async {
try {
final Map<String, dynamic> params = {
'limit': limit.toString(),
};
final response = await _apiClient.getWithParams('/analytics/token-usage-records', queryParameters: params);
final List<dynamic> data = response['data'] as List<dynamic>;
return data.map((item) => TokenUsageRecord(
id: item['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(),
model: item['model'] as String,
taskType: _mapFunctionToDisplay(item['taskType']?.toString() ?? ''),
inputTokens: item['inputTokens'] ?? 0,
outputTokens: item['outputTokens'] ?? 0,
cost: (item['cost'] ?? 0.0).toDouble(),
timestamp: parseBackendDateTime(item['timestamp']),
)).toList();
} catch (e) {
throw Exception('Failed to load token usage records: $e');
}
}
@override
Future<Map<String, dynamic>> getTodayTokenSummary() async {
try {
final response = await _apiClient.get('/analytics/today-summary');
return response['data'] as Map<String, dynamic>;
} catch (e) {
throw Exception('Failed to load today token summary: $e');
}
}
String _getViewModeString(AnalyticsViewMode mode) {
switch (mode) {
case AnalyticsViewMode.daily:
return 'daily';
case AnalyticsViewMode.monthly:
return 'monthly';
case AnalyticsViewMode.cumulative:
return 'cumulative';
case AnalyticsViewMode.range:
return 'range';
}
}
String _getModelColor(String model) {
final String m = model.toLowerCase();
if (m.contains('gpt')) return '#3B82F6';
if (m.contains('claude')) return '#8B5CF6';
if (m.contains('gemini')) return '#10B981';
if (m.contains('deepseek')) return '#F59E0B';
// 对未知模型使用稳定的哈希颜色,确保“不同的显示不同颜色”
final List<String> palette = ['#06B6D4', '#EF4444', '#22C55E', '#F97316', '#A855F7', '#0EA5E9'];
final int idx = model.hashCode.abs() % palette.length;
return palette[idx];
}
String _mapFunctionToDisplay(String functionKey) {
if (functionKey.isEmpty) return '';
try {
final feature = AIFeatureTypeHelper.fromApiString(functionKey);
return feature.displayName;
} catch (_) {
return functionKey;
}
}
// ----------
// Token 使用趋势数据补齐逻辑
// ----------
List<TokenUsageData> _postProcessTokenSeries({
required List<TokenUsageData> rawSeries,
required AnalyticsViewMode viewMode,
DateTime? startDate,
DateTime? endDate,
}) {
if (rawSeries.isEmpty) {
// 空数据时保留为空,让前端显示“暂无数据”
return rawSeries;
}
switch (viewMode) {
case AnalyticsViewMode.daily:
case AnalyticsViewMode.range:
return _fillDailySeries(
rawSeries: rawSeries,
explicitStart: startDate,
explicitEnd: endDate,
);
case AnalyticsViewMode.monthly:
return _fillMonthlySeries(
rawSeries: rawSeries,
explicitStart: startDate,
explicitEnd: endDate,
);
case AnalyticsViewMode.cumulative:
// 累计模式一般由后端计算。为避免误解语义不进行数值重算仅在只有单点时做最小可视化填充填充前置0
if (rawSeries.length > 1) return _sortByDateString(rawSeries);
return _fillDailySeries(
rawSeries: rawSeries,
explicitStart: startDate,
explicitEnd: endDate,
defaultWindowDays: 7,
);
}
}
List<TokenUsageData> _fillDailySeries({
required List<TokenUsageData> rawSeries,
DateTime? explicitStart,
DateTime? explicitEnd,
int defaultWindowDays = 7,
}) {
final List<TokenUsageData> sorted = _sortByDateString(rawSeries);
// 解析现有最早与最晚日期
final DateTime? firstDate = _tryParseDate(sorted.first.date);
final DateTime? lastDate = _tryParseDate(sorted.last.date);
// 对于仅有 MM-dd 的场景_tryParseDate 会用当前年填充,可能导致“跨年跳跃”。为了可视化体验:
// 若显式日期范围为空,则直接使用数据内最早/最晚的字符串顺序来确定窗口,不再随当前年错配。
DateTime start = _normalizeToDateOnly(explicitStart ?? firstDate ?? DateTime.now());
DateTime end = _normalizeToDateOnly(explicitEnd ?? lastDate ?? DateTime.now());
if (sorted.length == 1 && explicitStart == null && explicitEnd == null) {
// 仅单点数据时,默认展示 [last - (N-1)天, last] 的连续窗口
start = _normalizeToDateOnly(end.subtract(Duration(days: defaultWindowDays - 1)));
}
if (end.isBefore(start)) {
// 防御:若区间反转,交换
final tmp = start;
start = end;
end = tmp;
}
// 建立日期到数据的索引
final Map<String, TokenUsageData> dateToData = {
for (final d in sorted) _formatDate(d.date, AnalyticsViewMode.daily): d,
};
final List<TokenUsageData> filled = [];
DateTime cursor = start;
while (!cursor.isAfter(end)) {
final String key = _formatDateFromDateTime(cursor, AnalyticsViewMode.daily);
final TokenUsageData? existing = dateToData[key];
if (existing != null) {
filled.add(existing);
} else {
filled.add(TokenUsageData(
date: key,
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
modelTokens: const <String, int>{},
));
}
cursor = cursor.add(const Duration(days: 1));
}
return filled;
}
List<TokenUsageData> _fillMonthlySeries({
required List<TokenUsageData> rawSeries,
DateTime? explicitStart,
DateTime? explicitEnd,
int defaultWindowMonths = 6,
}) {
final List<TokenUsageData> sorted = _sortByDateString(rawSeries);
// 猜测月份:将字符串解析到当月第一天
final DateTime? firstMonth = _normalizeToMonthStart(_tryParseDate(sorted.first.date));
final DateTime? lastMonth = _normalizeToMonthStart(_tryParseDate(sorted.last.date));
DateTime start = explicitStart != null
? _normalizeToMonthStart(explicitStart)
: (firstMonth ?? _normalizeToMonthStart(DateTime.now()));
DateTime end = explicitEnd != null
? _normalizeToMonthStart(explicitEnd)
: (lastMonth ?? _normalizeToMonthStart(DateTime.now()));
if (sorted.length == 1 && explicitStart == null && explicitEnd == null) {
// 仅单点数据时,默认展示最近 N 个月
end = _normalizeToMonthStart(end);
start = _addMonths(end, -(defaultWindowMonths - 1));
}
if (end.isBefore(start)) {
final tmp = start;
start = end;
end = tmp;
}
// 索引yyyy-MM -> 数据
final Map<String, TokenUsageData> monthToData = {
for (final d in sorted) _formatDate(d.date, AnalyticsViewMode.monthly): d,
};
final List<TokenUsageData> filled = [];
DateTime cursor = start;
while (!cursor.isAfter(end)) {
final String key = _formatDateFromDateTime(cursor, AnalyticsViewMode.monthly);
final TokenUsageData? existing = monthToData[key];
if (existing != null) {
filled.add(existing);
} else {
filled.add(TokenUsageData(
date: key,
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
modelTokens: const <String, int>{},
));
}
cursor = _addMonths(cursor, 1);
}
return filled;
}
List<TokenUsageData> _sortByDateString(List<TokenUsageData> series) {
final List<TokenUsageData> copy = List<TokenUsageData>.from(series);
copy.sort((a, b) {
final DateTime? da = _tryParseDate(a.date);
final DateTime? db = _tryParseDate(b.date);
if (da == null && db == null) return 0;
if (da == null) return -1;
if (db == null) return 1;
return da.compareTo(db);
});
return copy;
}
DateTime? _tryParseDate(String raw) {
// 1) 直接解析 ISO / yyyy-MM-dd / yyyy-MM
final DateTime? direct = DateTime.tryParse(raw);
if (direct != null) return direct;
final now = DateTime.now();
try {
// 2) 显式识别 'yyyy-MM-dd'
if (RegExp(r'^\d{4}-\d{1,2}-\d{1,2}$').hasMatch(raw)) {
final p = raw.split('-');
final y = int.parse(p[0]);
final m = int.parse(p[1]);
final d = int.parse(p[2]);
return DateTime(y, m, d);
}
// 3) 显式识别 'yyyy-MM'
if (RegExp(r'^\d{4}-\d{1,2}$').hasMatch(raw)) {
final p = raw.split('-');
final y = int.parse(p[0]);
final m = int.parse(p[1]);
return DateTime(y, m, 1);
}
// 4) 显式识别 'MM-dd':视为当前年份的 月-日
if (RegExp(r'^\d{1,2}-\d{1,2}$').hasMatch(raw)) {
final p = raw.split('-');
final m = int.parse(p[0]);
final d = int.parse(p[1]);
return DateTime(now.year, m, d);
}
// 5) 宽松兜底若为两段且第一段长度为2按 MM-dd否则按 yyyy-MM(-dd)
final parts = raw.split('-');
if (parts.length == 2 && parts[0].length <= 2) {
final m = int.tryParse(parts[0]) ?? 1;
final d = int.tryParse(parts[1]) ?? 1;
return DateTime(now.year, m, d);
}
if (parts.length >= 2) {
final int y = int.tryParse(parts[0]) ?? now.year;
final int m = int.tryParse(parts[1]) ?? 1;
final int d = parts.length >= 3 ? (int.tryParse(parts[2]) ?? 1) : 1;
return DateTime(y, m, d);
}
} catch (_) {}
return null;
}
DateTime _normalizeToDateOnly(DateTime? dt) {
final DateTime base = dt ?? DateTime.now();
return DateTime(base.year, base.month, base.day);
}
DateTime _normalizeToMonthStart(DateTime? dt) {
final DateTime base = dt ?? DateTime.now();
return DateTime(base.year, base.month, 1);
}
DateTime _addMonths(DateTime dt, int delta) {
final int yearDelta = (dt.month - 1 + delta) ~/ 12;
final int newMonthIndex = (dt.month - 1 + delta) % 12;
final int newYear = dt.year + yearDelta;
final int newMonth = newMonthIndex + 1;
final int day = dt.day;
// 处理不同月份天数
final int lastDay = DateTime(newYear, newMonth + 1, 0).day;
final int safeDay = day > lastDay ? lastDay : day;
return DateTime(newYear, newMonth, safeDay);
}
String _formatDate(String raw, AnalyticsViewMode mode) {
final DateTime? dt = _tryParseDate(raw);
if (dt == null) return raw;
return _formatDateFromDateTime(dt, mode);
}
String _formatDateFromDateTime(DateTime dt, AnalyticsViewMode mode) {
final int y = dt.year;
final String m = dt.month.toString().padLeft(2, '0');
final String d = dt.day.toString().padLeft(2, '0');
switch (mode) {
case AnalyticsViewMode.daily:
case AnalyticsViewMode.range:
case AnalyticsViewMode.cumulative:
return '$y-$m-$d';
case AnalyticsViewMode.monthly:
return '$y-$m';
}
}
}

View File

@@ -0,0 +1,521 @@
import 'dart:async';
import 'dart:convert';
import 'package:ainoval/models/ai_request_models.dart';
import 'package:ainoval/models/chat_models.dart';
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/services/api_service/base/api_exception.dart';
import 'package:ainoval/services/api_service/base/sse_client.dart';
import 'package:ainoval/services/api_service/repositories/chat_repository.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:dio/dio.dart';
import 'package:flutter_client_sse/constants/sse_request_type_enum.dart';
/// 聊天仓库实现
class ChatRepositoryImpl implements ChatRepository {
ChatRepositoryImpl({
required this.apiClient,
});
final ApiClient apiClient;
// 🚀 修改为两层缓存映射用于存储会话的AI配置novelId -> sessionId -> config
static final Map<String, Map<String, UniversalAIRequest>> _cachedSessionConfigs = {};
/// 获取聊天会话列表 (流式) - 简化版
@override
Stream<ChatSession> fetchUserSessions(String userId, {String? novelId}) {
AppLogger.i('ChatRepositoryImpl', '获取用户会话流: userId=$userId, novelId=$novelId');
// 🚀 目前先使用原有API后续可以添加支持novelId的新API
try {
// TODO: 暂时使用原有API后续可以添加新的API方法
return apiClient.listAiChatUserSessionsStream(userId, novelId: novelId);
} catch (e, stackTrace) {
AppLogger.e('ChatRepositoryImpl', '发起获取用户会话流时出错 [同步]', e, stackTrace);
return Stream.error(
ApiExceptionHelper.fromException(e, '发起获取用户会话流失败'), stackTrace);
}
}
/// 创建新的聊天会话 (非流式)
@override
Future<ChatSession> createSession({
required String userId,
required String novelId,
String? modelName,
Map<String, dynamic>? metadata,
}) async {
try {
AppLogger.i('ChatRepositoryImpl',
'创建会话: userId=$userId, novelId=$novelId, modelName=$modelName');
final session = await apiClient.createAiChatSession(
userId: userId,
novelId: novelId,
modelName: modelName,
metadata: metadata,
);
AppLogger.i('ChatRepositoryImpl', '创建会话成功: sessionId=${session.id}');
return session;
} catch (e, stackTrace) {
AppLogger.e('ChatRepositoryImpl',
'创建会话失败: userId=$userId, novelId=$novelId', e, stackTrace);
throw ApiExceptionHelper.fromException(e, '创建会话失败');
}
}
/// 获取特定会话 (非流式) - 现在返回会话和AI配置的组合数据
@override
Future<ChatSession> getSession(String userId, String sessionId, {String? novelId}) async {
try {
AppLogger.i(
'ChatRepositoryImpl', '获取会话含AI配置: userId=$userId, sessionId=$sessionId, novelId=$novelId');
// 🚀 目前先使用原有API后续可以添加支持novelId的新API
final response = await apiClient.getAiChatSessionWithConfig(userId, sessionId, novelId: novelId);
AppLogger.i('ChatRepositoryImpl', '使用传统API获取会话');
final session = response['session'] as ChatSession;
AppLogger.i('ChatRepositoryImpl',
'获取会话成功: sessionId=$sessionId, title=${session.title}, hasAIConfig=${response["aiConfig"] != null}');
// 🚀 将AI配置信息缓存到两层映射中供后续使用
if (response['aiConfig'] != null && session.novelId != null) {
try {
final configData = response['aiConfig'];
Map<String, dynamic> configJson;
if (configData is String) {
final configString = configData as String;
if (configString.trim().isNotEmpty && configString != '{}') {
if (!configString.startsWith('{') || !configString.contains('"')) {
AppLogger.w('ChatRepositoryImpl', '检测到非标准JSON格式跳过解析');
} else {
try {
configJson = jsonDecode(configString);
final config = UniversalAIRequest.fromJson(configJson);
// 🚀 将配置缓存到两层映射中
_cachedSessionConfigs[session.novelId!] ??= {};
_cachedSessionConfigs[session.novelId!]![sessionId] = config;
AppLogger.i('ChatRepositoryImpl',
'成功缓存会话AI配置: novelId=${session.novelId}, sessionId=$sessionId, requestType=${config.requestType.value}');
} catch (e) {
AppLogger.e('ChatRepositoryImpl', '解析AI配置JSON失败: $e');
}
}
}
} else if (configData is Map<String, dynamic>) {
try {
final config = UniversalAIRequest.fromJson(configData);
// 🚀 将配置缓存到两层映射中
_cachedSessionConfigs[session.novelId!] ??= {};
_cachedSessionConfigs[session.novelId!]![sessionId] = config;
AppLogger.i('ChatRepositoryImpl',
'成功缓存会话AI配置: novelId=${session.novelId}, sessionId=$sessionId, requestType=${config.requestType.value}');
} catch (e) {
AppLogger.e('ChatRepositoryImpl', '解析AI配置Map失败: $e');
}
}
} catch (e) {
AppLogger.w('ChatRepositoryImpl', '缓存AI配置失败但不影响会话加载: $e');
}
}
return session;
} catch (e, stackTrace) {
AppLogger.e('ChatRepositoryImpl',
'获取会话失败: userId=$userId, sessionId=$sessionId, novelId=$novelId', e, stackTrace);
throw ApiExceptionHelper.fromException(e, '获取会话失败');
}
}
/// 获取会话的AI配置 (非流式) - 现在从两层缓存中获取
@override
Future<UniversalAIRequest?> getSessionAIConfig(String userId, String sessionId, {String? novelId}) async {
AppLogger.i('ChatRepositoryImpl',
'从缓存获取会话AI配置: userId=$userId, sessionId=$sessionId, novelId=$novelId');
// 🚀 从两层缓存中获取配置
if (novelId != null) {
final cachedConfig = _cachedSessionConfigs[novelId]?[sessionId];
if (cachedConfig != null) {
AppLogger.i('ChatRepositoryImpl',
'找到缓存的会话AI配置: novelId=$novelId, sessionId=$sessionId, requestType=${cachedConfig.requestType.value}');
return cachedConfig;
}
} else {
// 如果没有novelId尝试在所有novel中查找
for (final novelConfigs in _cachedSessionConfigs.values) {
final cachedConfig = novelConfigs[sessionId];
if (cachedConfig != null) {
AppLogger.i('ChatRepositoryImpl',
'在缓存中找到会话AI配置: sessionId=$sessionId, requestType=${cachedConfig.requestType.value}');
return cachedConfig;
}
}
}
AppLogger.i('ChatRepositoryImpl',
'缓存中没有找到会话AI配置: novelId=$novelId, sessionId=$sessionId');
return null;
}
/// 获取缓存的会话配置(静态方法,供其他类使用)
static UniversalAIRequest? getCachedSessionConfig(String sessionId, {String? novelId}) {
if (novelId != null) {
return _cachedSessionConfigs[novelId]?[sessionId];
} else {
// 如果没有novelId尝试在所有novel中查找
for (final novelConfigs in _cachedSessionConfigs.values) {
final config = novelConfigs[sessionId];
if (config != null) return config;
}
return null;
}
}
/// 缓存会话配置(静态方法,供其他类使用)
static void cacheSessionConfig(String sessionId, UniversalAIRequest config, {String? novelId}) {
final targetNovelId = novelId ?? config.novelId;
if (targetNovelId != null) {
_cachedSessionConfigs[targetNovelId] ??= {};
_cachedSessionConfigs[targetNovelId]![sessionId] = config;
AppLogger.i('ChatRepositoryImpl', '缓存会话AI配置: novelId=$targetNovelId, sessionId=$sessionId');
} else {
AppLogger.w('ChatRepositoryImpl', '无法缓存会话配置缺少novelId信息');
}
}
/// 清除会话配置缓存
static void clearSessionConfigCache(String sessionId, {String? novelId}) {
if (novelId != null) {
_cachedSessionConfigs[novelId]?.remove(sessionId);
AppLogger.i('ChatRepositoryImpl', '清除会话AI配置缓存: novelId=$novelId, sessionId=$sessionId');
} else {
// 如果没有novelId清除所有novel中的该sessionId
for (final novelConfigs in _cachedSessionConfigs.values) {
novelConfigs.remove(sessionId);
}
AppLogger.i('ChatRepositoryImpl', '清除所有小说中的会话AI配置缓存: sessionId=$sessionId');
}
}
/// 清除整个小说的配置缓存
static void clearNovelConfigCache(String novelId) {
_cachedSessionConfigs.remove(novelId);
AppLogger.i('ChatRepositoryImpl', '清除小说的所有AI配置缓存: novelId=$novelId');
}
/// 更新会话 (非流式)
@override
Future<ChatSession> updateSession({
required String userId,
required String sessionId,
required Map<String, dynamic> updates,
String? novelId,
}) async {
try {
AppLogger.i('ChatRepositoryImpl',
'更新会话: userId=$userId, sessionId=$sessionId, novelId=$novelId, updates=$updates');
// 🚀 目前先使用原有API后续可以添加支持novelId的新API
final updatedSession = await apiClient.updateAiChatSession(
userId: userId,
sessionId: sessionId,
updates: updates,
novelId: novelId,
);
AppLogger.i('ChatRepositoryImpl',
'更新会话成功: sessionId=$sessionId, title=${updatedSession.title}');
return updatedSession;
} catch (e, stackTrace) {
AppLogger.e('ChatRepositoryImpl',
'更新会话失败: userId=$userId, sessionId=$sessionId, novelId=$novelId', e, stackTrace);
throw ApiExceptionHelper.fromException(e, '更新会话失败');
}
}
/// 删除会话 (非流式)
@override
Future<void> deleteSession(String userId, String sessionId, {String? novelId}) async {
try {
AppLogger.i(
'ChatRepositoryImpl', '删除会话: userId=$userId, sessionId=$sessionId, novelId=$novelId');
// 🚀 目前先使用原有API后续可以添加支持novelId的新API
await apiClient.deleteAiChatSession(userId, sessionId, novelId: novelId);
// 清除该会话的配置缓存
clearSessionConfigCache(sessionId, novelId: novelId);
AppLogger.i('ChatRepositoryImpl', '删除会话成功: sessionId=$sessionId');
} catch (e, stackTrace) {
AppLogger.e('ChatRepositoryImpl',
'删除会话失败: userId=$userId, sessionId=$sessionId, novelId=$novelId', e, stackTrace);
throw ApiExceptionHelper.fromException(e, '删除会话失败');
}
}
/// 发送消息并获取响应 (非流式)
@override
Future<ChatMessage> sendMessage({
required String userId,
required String sessionId,
required String content,
UniversalAIRequest? config,
Map<String, dynamic>? metadata,
String? configId,
String? novelId,
}) async {
try {
AppLogger.i('ChatRepositoryImpl',
'发送消息: userId=$userId, sessionId=$sessionId, novelId=$novelId, configId=$configId, hasConfig=${config != null}, contentLength=${content.length}');
// 🚀 如果有配置将配置数据添加到metadata中
Map<String, dynamic>? finalMetadata = metadata ?? {};
if (config != null) {
// 将配置序列化到metadata中
finalMetadata['aiConfig'] = config.toApiJson();
AppLogger.d('ChatRepositoryImpl', '添加AI配置到metadata配置类型: ${config.requestType.value}');
}
// 🚀 目前先使用原有API后续可以添加支持novelId的新API
final messageResponse = await apiClient.sendAiChatMessage(
userId: userId,
sessionId: sessionId,
content: content,
metadata: finalMetadata,
novelId: novelId,
);
AppLogger.i('ChatRepositoryImpl',
'收到AI响应: sessionId=$sessionId, messageId=${messageResponse.id}, contentLength=${messageResponse.content.length}');
return messageResponse;
} catch (e, stackTrace) {
AppLogger.e('ChatRepositoryImpl',
'发送消息失败: userId=$userId, sessionId=$sessionId, novelId=$novelId', e, stackTrace);
throw ApiExceptionHelper.fromException(e, '发送消息失败');
}
}
/// 流式发送消息并获取响应 - 简化版
@override
Stream<ChatMessage> streamMessage({
required String userId,
required String sessionId,
required String content,
UniversalAIRequest? config,
Map<String, dynamic>? metadata,
String? configId,
String? novelId,
}) {
AppLogger.i('ChatRepositoryImpl',
'开始流式消息: userId=$userId, sessionId=$sessionId, novelId=$novelId, configId=$configId, hasConfig=${config != null}');
try {
// 🚀 准备配置数据
Map<String, dynamic>? configData;
Map<String, dynamic>? finalMetadata = metadata ?? {};
if (config != null) {
// 将配置序列化
configData = config.toApiJson();
// 同时添加到metadata中以保持兼容性
finalMetadata['aiConfig'] = configData;
AppLogger.d('ChatRepositoryImpl', '添加AI配置到请求配置类型: ${config.requestType.value}');
}
// 🚀 构建请求体根据是否有novelId选择不同的请求格式
Map<String, dynamic> requestBody = {
'userId': userId,
'sessionId': sessionId,
'content': content,
'metadata': finalMetadata,
};
if (novelId != null) {
requestBody['novelId'] = novelId;
}
// 🚀 使用SSE方式发送流式消息与后端的标准SSE格式匹配
return SseClient().streamEvents<ChatMessage>(
path: '/ai-chat/messages/stream',
method: SSERequestType.POST,
body: requestBody,
parser: (json) {
try {
return ChatMessage.fromJson(json);
} catch (e) {
AppLogger.e('ChatRepositoryImpl', '解析ChatMessage失败: $e, json: $json');
throw ApiException(-1, '解析聊天响应失败: $e');
}
},
eventName: 'chat-message', // 🚀 使用与后端一致的事件名称
).where((message) {
// 🚀 首先检查消息是否属于当前会话
if (message.sessionId != sessionId) {
AppLogger.v('ChatRepositoryImpl', '过滤掉其他会话的消息: sessionId=${message.sessionId}, 当前会话=$sessionId');
return false;
}
// 🚀 过滤掉心跳信号但保留STREAM_CHUNK消息用于打字机效果
final isHeartbeat = message.content == 'heartbeat';
if (isHeartbeat) {
AppLogger.v('ChatRepositoryImpl', '过滤掉心跳信号: sessionId=${message.sessionId}');
return false;
}
// 🚀 保留流式块消息用于打字机效果
if (message.status == MessageStatus.streaming) {
//AppLogger.v('ChatRepositoryImpl', '保留流式块消息用于打字机效果: ${message.content}');
return true;
}
// 只保留有实际ID和内容的完整消息
final isCompleteMessage = message.id.isNotEmpty && !message.id.startsWith('temp_chunk_') && message.content.isNotEmpty;
if (isCompleteMessage) {
AppLogger.i('ChatRepositoryImpl', '📘 接收到完整消息: messageId=${message.id}, contentLength=${message.content.length}');
}
return isCompleteMessage;
});
} catch (e, stackTrace) {
AppLogger.e('ChatRepositoryImpl', '发起流式消息请求时出错 [同步]', e, stackTrace);
return Stream.error(
ApiExceptionHelper.fromException(e, '发起流式消息请求失败'), stackTrace);
}
}
/// 获取会话消息历史 (流式) - 简化版
@override
Stream<ChatMessage> getMessageHistory(
String userId,
String sessionId, {
int limit = 100,
String? novelId,
}) {
AppLogger.i('ChatRepositoryImpl',
'获取消息历史流: userId=$userId, sessionId=$sessionId, novelId=$novelId, limit=$limit');
try {
// 🚀 目前先使用原有API后续可以添加支持novelId的新API
return apiClient.getAiChatMessageHistoryStream(userId, sessionId,
limit: limit, novelId: novelId);
} catch (e, stackTrace) {
AppLogger.e('ChatRepositoryImpl', '发起获取消息历史流请求时出错 [同步]', e, stackTrace);
return Stream.error(
ApiExceptionHelper.fromException(e, '发起获取消息历史流失败'), stackTrace);
}
}
/// 获取特定消息 (非流式)
@override
Future<ChatMessage> getMessage(String userId, String messageId) async {
try {
AppLogger.i(
'ChatRepositoryImpl', '获取消息: userId=$userId, messageId=$messageId');
final message = await apiClient.getAiChatMessage(userId, messageId);
AppLogger.i('ChatRepositoryImpl',
'获取消息成功: messageId=$messageId, role=${message.role}');
return message;
} catch (e, stackTrace) {
AppLogger.e('ChatRepositoryImpl',
'获取消息失败: userId=$userId, messageId=$messageId', e, stackTrace);
throw ApiExceptionHelper.fromException(e, '获取消息失败');
}
}
/// 删除消息 (非流式)
@override
Future<void> deleteMessage(String userId, String messageId) async {
try {
AppLogger.i(
'ChatRepositoryImpl', '删除消息: userId=$userId, messageId=$messageId');
await apiClient.deleteAiChatMessage(userId, messageId);
AppLogger.i('ChatRepositoryImpl', '删除消息成功: messageId=$messageId');
} catch (e, stackTrace) {
AppLogger.e('ChatRepositoryImpl',
'删除消息失败: userId=$userId, messageId=$messageId', e, stackTrace);
throw ApiExceptionHelper.fromException(e, '删除消息失败');
}
}
/// 获取会话消息数量 (非流式)
@override
Future<int> countSessionMessages(String sessionId) async {
try {
AppLogger.i('ChatRepositoryImpl', '统计会话消息数量: sessionId=$sessionId');
final count = await apiClient.countAiChatSessionMessages(sessionId);
AppLogger.i('ChatRepositoryImpl',
'统计会话消息数量成功: sessionId=$sessionId, count=$count');
return count;
} catch (e, stackTrace) {
AppLogger.e('ChatRepositoryImpl', '统计会话消息数量失败: sessionId=$sessionId', e,
stackTrace);
throw ApiExceptionHelper.fromException(e, '统计会话消息数量失败');
}
}
/// 获取用户会话数量 (非流式)
@override
Future<int> countUserSessions(String userId, {String? novelId}) async {
try {
AppLogger.i('ChatRepositoryImpl', '统计用户会话数量: userId=$userId, novelId=$novelId');
// 🚀 目前先使用原有API后续可以添加支持novelId的新API
final count = await apiClient.countAiChatUserSessions(userId);
AppLogger.i(
'ChatRepositoryImpl', '统计用户会话数量成功: userId=$userId, novelId=$novelId, count=$count');
return count;
} catch (e, stackTrace) {
AppLogger.e(
'ChatRepositoryImpl', '统计用户会话数量失败: userId=$userId, novelId=$novelId', e, stackTrace);
throw ApiExceptionHelper.fromException(e, '统计用户会话数量失败');
}
}
}
// 辅助扩展方法,如果 ApiException 没有 fromException
extension ApiExceptionHelper on ApiException {
static ApiException fromException(dynamic e, String defaultMessage) {
if (e is ApiException) {
return e;
} else if (e is DioException) {
// 现在可以识别 DioException 了
final statusCode = e.response?.statusCode ?? -1;
// 尝试获取后端返回的错误信息,如果失败则使用 DioException 的 message
final backendMessage = _tryGetBackendMessage(e.response);
final detailedMessage = backendMessage ?? e.message ?? defaultMessage;
return ApiException(statusCode, '$defaultMessage: $detailedMessage');
} else {
return ApiException(-1, '$defaultMessage: ${e.toString()}');
}
}
// 尝试从 Response 中提取后端错误信息
static String? _tryGetBackendMessage(Response? response) {
if (response?.data != null) {
try {
final data = response!.data;
if (data is Map<String, dynamic>) {
// 查找常见的错误消息字段
if (data.containsKey('message') && data['message'] is String) {
return data['message'];
}
if (data.containsKey('error') && data['error'] is String) {
return data['error'];
}
if (data.containsKey('detail') && data['detail'] is String) {
return data['detail'];
}
} else if (data is String && data.isNotEmpty) {
return data; // 如果响应体直接是错误字符串
}
} catch (_) {
// 忽略解析错误
}
}
return null;
}
}

View File

@@ -0,0 +1,29 @@
import '../../../../models/user_credit.dart';
import '../../../../utils/logger.dart';
import '../../base/api_client.dart';
import '../credit_repository.dart';
/// 用户积分仓库实现
class CreditRepositoryImpl implements CreditRepository {
final ApiClient _apiClient;
static const String _tag = 'CreditRepositoryImpl';
CreditRepositoryImpl({required ApiClient apiClient}) : _apiClient = apiClient;
@override
Future<UserCredit> getUserCredits() async {
try {
AppLogger.i(_tag, '获取用户积分余额');
final rawData = await _apiClient.getUserCredits();
// 转换为UserCredit对象
final userCredit = UserCredit.fromJson(rawData);
AppLogger.i(_tag, '获取用户积分余额成功: ${userCredit.credits}');
return userCredit;
} catch (e, stackTrace) {
AppLogger.e(_tag, '获取用户积分余额失败', e, stackTrace);
rethrow;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,122 @@
import 'dart:convert';
import 'package:ainoval/models/next_outline/next_outline_dto.dart';
import 'package:ainoval/models/next_outline/outline_generation_chunk.dart';
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/services/api_service/base/api_exception.dart';
import 'package:ainoval/services/api_service/base/sse_client.dart';
import 'package:ainoval/services/api_service/repositories/next_outline_repository.dart';
import 'package:flutter_client_sse/constants/sse_request_type_enum.dart';
import '../../../../utils/logger.dart';
/// 剧情推演仓库实现
class NextOutlineRepositoryImpl implements NextOutlineRepository {
NextOutlineRepositoryImpl({
required this.apiClient,
});
final ApiClient apiClient;
final String _tag = 'NextOutlineRepositoryImpl';
@override
Stream<OutlineGenerationChunk> generateNextOutlinesStream(
String novelId,
GenerateNextOutlinesRequest request
) {
AppLogger.i(_tag, '流式生成剧情大纲: novelId=$novelId, startChapter=${request.startChapterId}, endChapter=${request.endChapterId}, numOptions=${request.numOptions}');
return SseClient().streamEvents<OutlineGenerationChunk>(
path: '/novels/$novelId/next-outlines/generate-stream',
method: SSERequestType.POST,
body: request.toJson(),
parser: (json) {
// 增强解析器的错误处理: 首先检查是否是已知的错误格式
if (json is Map<String, dynamic> && json.containsKey('code') && json.containsKey('message')) {
final errorMessage = json['message'] as String? ?? 'Unknown server error';
final errorCodeString = json['code'] as String?;
final errorCode = int.tryParse(errorCodeString ?? '') ?? -1; // 尝试解析为int失败则为-1
AppLogger.e(_tag, '服务器返回已知错误格式: code=${json['code']}, message=$errorMessage');
throw ApiException(errorCode, errorMessage); // 使用int类型的errorCode
}
// 再检查是否包含 'error' 字段的值是否非空 (兼容旧的或不同的错误格式)
else if (json is Map<String, dynamic> && json['error'] != null) {
final errorMessage = json['error'] as String? ?? 'Unknown server error';
AppLogger.e(_tag, '服务器返回错误字段: $errorMessage');
throw ApiException(-1, errorMessage); // 默认错误码-1
}
// 如果不是错误格式,则尝试解析为正常数据块
try {
return OutlineGenerationChunk.fromJson(json);
} catch (e, stackTrace) {
AppLogger.e(_tag, '解析OutlineGenerationChunk失败: $e, json: $json'); // 移除 stackTrace
// 抛出更具体的解析异常
throw ApiException(-1, '解析响应失败: $e');
}
},
eventName: 'outline-chunk',
);
}
@override
Stream<OutlineGenerationChunk> regenerateOutlineOption(
String novelId,
RegenerateOptionRequest request
) {
AppLogger.i(_tag, '重新生成单个剧情大纲选项: novelId=$novelId, optionId=${request.optionId}, configId=${request.selectedConfigId}');
return SseClient().streamEvents<OutlineGenerationChunk>(
path: '/novels/$novelId/next-outlines/regenerate-option',
method: SSERequestType.POST,
body: request.toJson(),
parser: (json) {
// 增强解析器的错误处理: 首先检查是否是已知的错误格式
if (json is Map<String, dynamic> && json.containsKey('code') && json.containsKey('message')) {
final errorMessage = json['message'] as String? ?? 'Unknown server error';
final errorCodeString = json['code'] as String?;
final errorCode = int.tryParse(errorCodeString ?? '') ?? -1; // 尝试解析为int失败则为-1
AppLogger.e(_tag, '服务器返回已知错误格式: code=${json['code']}, message=$errorMessage');
throw ApiException(errorCode, errorMessage); // 使用int类型的errorCode
}
// 再检查是否包含 'error' 字段的值是否非空 (兼容旧的或不同的错误格式)
else if (json is Map<String, dynamic> && json['error'] != null) {
final errorMessage = json['error'] as String? ?? 'Unknown server error';
AppLogger.e(_tag, '服务器返回错误字段: $errorMessage');
throw ApiException(-1, errorMessage); // 默认错误码-1
}
// 如果不是错误格式,则尝试解析为正常数据块
try {
return OutlineGenerationChunk.fromJson(json);
} catch (e, stackTrace) {
AppLogger.e(_tag, '解析OutlineGenerationChunk失败: $e, json: $json'); // 移除 stackTrace
// 抛出更具体的解析异常
throw ApiException(-1, '解析响应失败: $e');
}
},
eventName: 'outline-chunk',
);
}
@override
Future<SaveNextOutlineResponse> saveNextOutline(
String novelId,
SaveNextOutlineRequest request
) async {
AppLogger.i(_tag, '保存剧情大纲: novelId=$novelId, outlineId=${request.outlineId}, insertType=${request.insertType}');
try {
final response = await apiClient.post(
'/novels/$novelId/next-outlines/save',
data: request.toJson(),
);
return SaveNextOutlineResponse.fromJson(response);
} catch (e) {
AppLogger.e(_tag, '保存剧情大纲失败', e);
rethrow;
}
}
}

View File

@@ -0,0 +1,54 @@
import 'package:ainoval/models/novel_setting_item.dart';
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/services/api_service/repositories/novel_ai_repository.dart';
import 'package:ainoval/utils/logger.dart';
class NovelAIRepositoryImpl implements NovelAIRepository {
final ApiClient apiClient;
NovelAIRepositoryImpl({required this.apiClient});
@override
Future<List<NovelSettingItem>> generateNovelSettings({
required String novelId,
required String startChapterId,
String? endChapterId,
required List<String> settingTypes,
required int maxSettingsPerType,
required String additionalInstructions,
}) async {
AppLogger.i('NovelAIRepoImpl', 'Generating settings for novel $novelId');
try {
final response = await apiClient.post(
// Make sure the path matches your backend routing exactly
'/novels/$novelId/ai/generate-settings',
data: {
'startChapterId': startChapterId,
if (endChapterId != null && endChapterId.isNotEmpty) 'endChapterId': endChapterId,
'settingTypes': settingTypes,
'maxSettingsPerType': maxSettingsPerType,
'additionalInstructions': additionalInstructions,
},
);
if (response is List) {
final items = response
.map((json) => NovelSettingItem.fromJson(json as Map<String, dynamic>))
.toList();
AppLogger.i('NovelAIRepoImpl', 'Successfully generated ${items.length} setting items.');
return items;
} else if (response is Map<String, dynamic> && response.containsKey('error')) {
// Handle structured error from backend if any
AppLogger.e('NovelAIRepoImpl', 'Error from backend: ${response['message']}');
throw Exception('Failed to generate settings: ${response['message']}');
} else {
AppLogger.e('NovelAIRepoImpl', 'Unexpected response format for generateNovelSettings: $response');
throw Exception('Failed to parse generated settings: Unexpected response format');
}
} catch (e, stackTrace) {
AppLogger.e('NovelAIRepoImpl', 'Failed to generate novel settings via API', e, stackTrace);
// Rethrow a more specific error or a generic one
throw Exception('API call failed for generating settings: ${e.toString()}');
}
}
}

View File

@@ -0,0 +1,920 @@
import 'package:ainoval/config/app_config.dart';
import 'package:ainoval/models/import_status.dart';
import 'package:ainoval/models/novel_structure.dart';
import 'package:ainoval/models/scene_version.dart';
import 'package:ainoval/models/chapters_for_preload_dto.dart';
import 'package:ainoval/models/editor_settings.dart';
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/services/api_service/base/api_exception.dart';
import 'package:ainoval/services/api_service/base/sse_client.dart';
import 'package:ainoval/services/api_service/repositories/novel_repository.dart';
import 'package:ainoval/utils/date_time_parser.dart';
import 'package:ainoval/utils/logger.dart';
/// 小说仓库实现
class NovelRepositoryImpl implements NovelRepository {
/// 工厂构造函数
factory NovelRepositoryImpl() {
return _instance;
}
/// 内部构造函数
NovelRepositoryImpl._internal({
ApiClient? apiClient,
SseClient? sseClient,
}) : _apiClient = apiClient ?? ApiClient(),
_sseClient = sseClient ?? SseClient();
/// 创建NovelRepositoryImpl单例
static final NovelRepositoryImpl _instance = NovelRepositoryImpl._internal();
final ApiClient _apiClient;
final SseClient _sseClient;
/// 获取当前用户ID
String? get _currentUserId => AppConfig.userId;
/// 获取当前认证 Token
String? get _authToken => AppConfig.authToken;
/// 工厂方法获取单例
static NovelRepositoryImpl getInstance() {
return _instance;
}
/// 获取所有小说
@override
Future<List<Novel>> fetchNovels() async {
try {
final userId = _currentUserId;
if (userId == null) {
throw ApiException(401, '未登录或用户ID不可用');
}
final data = await _apiClient.getNovelsByAuthor(userId);
return _convertToNovelList(data);
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'获取小说列表失败',
e);
rethrow;
}
}
/// 获取单个小说
@override
Future<Novel> fetchNovel(String id) async {
try {
final data = await _apiClient.getNovelDetailById(id);
final novel = _convertToSingleNovel(data);
if (novel == null) {
throw ApiException(404, '小说不存在或数据格式不正确');
}
return novel;
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'获取小说详情失败',
e);
rethrow;
}
}
/// 创建小说
@override
Future<Novel> createNovel(String title,
{String? description, String? coverImage}) async {
try {
// 获取当前用户ID
final userId = _currentUserId;
if (userId == null) {
throw ApiException(401, '未登录或用户ID不可用');
}
// 准备请求体
final body = {
'title': title,
'description': description ?? '',
'coverImage': coverImage,
'author': {'id': userId, 'username': AppConfig.username ?? 'user'},
'status': 'draft',
'structure': {'acts': []},
'metadata': {'wordCount': 0, 'readTime': 0, 'version': 1}
};
// 发送创建请求
final data = await _apiClient.createNovel(body);
final novel = _convertToSingleNovel(data);
if (novel == null) {
throw ApiException(-1, '创建小说失败:服务器返回的数据格式不正确');
}
return novel;
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'创建小说失败',
e);
throw ApiException(-1, '创建小说失败: $e');
}
}
/// 根据作者ID获取小说列表
@override
Future<List<Novel>> fetchNovelsByAuthor(String authorId) async {
try {
final data = await _apiClient.getNovelsByAuthor(authorId);
return _convertToNovelList(data);
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'获取作者小说列表失败',
e);
rethrow;
}
}
/// 搜索小说
@override
Future<List<Novel>> searchNovelsByTitle(String title) async {
try {
final data = await _apiClient.searchNovelsByTitle(title);
return _convertToNovelList(data);
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'搜索小说失败',
e);
rethrow;
}
}
/// 删除小说
@override
Future<void> deleteNovel(String id) async {
try {
await _apiClient.deleteNovel(id);
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'删除小说失败',
e);
throw ApiException(-1, '删除小说失败: $e');
}
}
/// 获取场景内容
@override
Future<Scene> fetchSceneContent(
String novelId, String actId, String chapterId, String sceneId) async {
try {
final data = await _apiClient.getSceneById(novelId, chapterId, sceneId);
return _convertToSceneModel(data);
} catch (e) {
// 如果获取失败特别是404可能场景尚未创建返回一个空场景
if (e is ApiException && e.statusCode == 404) {
AppLogger.w(
'Services/api_service/repositories/impl/novel_repository_impl',
'场景 $sceneId 未找到,返回空场景');
return Scene.createDefault(sceneId);
}
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'获取场景内容失败',
e);
rethrow;
}
}
/// 更新场景内容并保存历史版本
@override
Future<Scene> updateSceneContentWithHistory(String novelId, String chapterId,
String sceneId, String content, String userId, String reason) async {
try {
// 发送API请求
final data = await _apiClient.updateSceneWithHistory(
novelId, chapterId, sceneId, content, userId, reason);
// 解析响应
return Scene.fromJson(data);
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'更新场景内容并保存历史版本失败',
e);
throw ApiException(500, '更新场景内容并保存历史版本失败: $e');
}
}
/// 获取场景的历史版本列表
@override
Future<List<SceneHistoryEntry>> getSceneHistory(
String novelId, String chapterId, String sceneId) async {
try {
// 发送API请求
final data =
await _apiClient.getSceneHistory(novelId, chapterId, sceneId);
// 解析响应
return (data as List).map((e) => SceneHistoryEntry.fromJson(e)).toList();
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'获取场景历史版本失败',
e);
throw ApiException(500, '获取场景历史版本失败: $e');
}
}
/// 恢复场景到指定的历史版本
@override
Future<Scene> restoreSceneVersion(String novelId, String chapterId,
String sceneId, int historyIndex, String userId, String reason) async {
try {
// 发送API请求
// 注意API 路径中的 historyIndex 可能需要调整为 versionId这取决于后端实现
// 假设后端接受 historyIndex
final data = await _apiClient.restoreSceneVersion(
novelId, chapterId, sceneId, historyIndex, userId, reason);
// 解析响应
return Scene.fromJson(data);
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'恢复历史版本失败',
e);
throw ApiException(500, '恢复历史版本失败: $e');
}
}
/// 对比两个场景版本
@override
Future<SceneVersionDiff> compareSceneVersions(
String novelId,
String chapterId,
String sceneId,
int versionIndex1,
int versionIndex2) async {
try {
// 发送API请求
// 注意API 路径中的 versionIndex 可能需要调整为 versionId这取决于后端实现
// 假设后端接受 versionIndex
final data = await _apiClient.compareSceneVersions(
novelId, chapterId, sceneId, versionIndex1, versionIndex2);
// 解析响应
return SceneVersionDiff.fromJson(data);
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'比较版本差异失败',
e);
throw ApiException(500, '比较版本差异失败: $e');
}
}
/// 导入小说文件
@override
Future<String> importNovel(List<int> fileBytes, String fileName) async {
try {
return await _apiClient.importNovel(fileBytes, fileName);
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'导入小说文件失败',
e);
rethrow;
}
}
/// 获取导入任务状态流
@override
Stream<ImportStatus> getImportStatus(String jobId) {
final String path = '/novels/import/$jobId/status';
final String connectionId = 'import_$jobId';
try {
AppLogger.i(
'Services/api_service/repositories/impl/novel_repository_impl',
'Subscribing to SSE stream for job: $jobId at path: $path using SseClient');
return _sseClient.streamEvents<ImportStatus>(
path: path,
parser: ImportStatus.fromJson,
eventName: 'import-status',
connectionId: connectionId,
);
} catch (e, stack) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'获取导入状态流失败 (同步)',
e,
stack);
return Stream.error(
e is ApiException ? e : ApiException(-1, '获取导入状态流失败: $e'), stack);
}
}
/// 取消导入任务
@override
Future<bool> cancelImport(String jobId) async {
final String connectionId = 'import_$jobId';
try {
AppLogger.i(
'Services/api_service/repositories/impl/novel_repository_impl',
'取消导入任务 $jobId: 发送请求到服务器');
// 首先通过API向服务器发送取消请求
final bool apiCanceled = await _apiClient.cancelImport(jobId);
// 然后尝试取消SSE连接
final bool sseCanceled = await _sseClient.cancelConnection(connectionId);
// 只要有一个成功就算成功
final bool success = apiCanceled || sseCanceled;
AppLogger.i(
'Services/api_service/repositories/impl/novel_repository_impl',
'取消导入任务 $jobId: ${success ? '成功' : '失败或已完成'} (API: $apiCanceled, SSE: $sseCanceled)');
return success;
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'取消导入任务失败',
e);
return false;
}
}
// === 新的三步导入流程方法实现 ===
/// 第一步上传文件获取预览会话ID
@override
Future<String> uploadFileForPreview(List<int> fileBytes, String fileName) async {
try {
AppLogger.i(
'Services/api_service/repositories/impl/novel_repository_impl',
'上传文件获取预览: fileName=$fileName, size=${fileBytes.length}');
final result = await _apiClient.uploadFileForPreview(fileBytes, fileName);
AppLogger.i(
'Services/api_service/repositories/impl/novel_repository_impl',
'文件上传成功预览会话ID: $result');
return result;
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'上传文件获取预览失败',
e);
throw e;
}
}
/// 第二步:获取导入预览
@override
Future<Map<String, dynamic>> getImportPreview({
required String fileSessionId,
String? customTitle,
int? chapterLimit,
bool enableSmartContext = true,
bool enableAISummary = false,
String? aiConfigId,
int previewChapterCount = 10,
}) async {
try {
AppLogger.i(
'Services/api_service/repositories/impl/novel_repository_impl',
'获取导入预览: sessionId=$fileSessionId, title=$customTitle, chapterLimit=$chapterLimit');
final responseData = await _apiClient.getImportPreview(
fileSessionId: fileSessionId,
customTitle: customTitle,
chapterLimit: chapterLimit,
enableSmartContext: enableSmartContext,
enableAISummary: enableAISummary,
aiConfigId: aiConfigId,
previewChapterCount: previewChapterCount,
);
AppLogger.i(
'Services/api_service/repositories/impl/novel_repository_impl',
'获取导入预览成功: 响应数据获取完成');
return responseData;
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'获取导入预览失败',
e);
rethrow;
}
}
/// 第三步:确认并开始导入
@override
Future<String> confirmAndStartImport({
required String previewSessionId,
required String finalTitle,
List<int>? selectedChapterIndexes,
bool enableSmartContext = true,
bool enableAISummary = false,
String? aiConfigId,
}) async {
try {
AppLogger.i(
'Services/api_service/repositories/impl/novel_repository_impl',
'确认并开始导入: sessionId=$previewSessionId, title=$finalTitle, chapters=${selectedChapterIndexes?.length ?? "全部"}');
final jobId = await _apiClient.confirmAndStartImport(
previewSessionId: previewSessionId,
finalTitle: finalTitle,
selectedChapterIndexes: selectedChapterIndexes,
enableSmartContext: enableSmartContext,
enableAISummary: enableAISummary,
aiConfigId: aiConfigId,
);
AppLogger.i(
'Services/api_service/repositories/impl/novel_repository_impl',
'确认导入成功任务ID: $jobId');
return jobId;
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'确认并开始导入失败',
e);
throw e;
}
}
/// 清理预览会话
@override
Future<void> cleanupPreviewSession(String previewSessionId) async {
try {
AppLogger.i(
'Services/api_service/repositories/impl/novel_repository_impl',
'清理预览会话: sessionId=$previewSessionId');
await _apiClient.cleanupPreviewSession(previewSessionId);
AppLogger.i(
'Services/api_service/repositories/impl/novel_repository_impl',
'预览会话清理成功');
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'清理预览会话失败',
e);
throw e;
}
}
/// 将WebFlux响应转换为Novel列表
List<Novel> _convertToNovelList(dynamic data) {
final processedData = _handleFluxResponse(data);
if (processedData == null) {
return [];
}
if (processedData is List) {
// 处理列表数据
return processedData.map((item) {
if (item is Map<String, dynamic>) {
return _convertToNovelModel(item);
} else {
AppLogger.w(
'Services/api_service/repositories/impl/novel_repository_impl',
'警告:列表中的项目不是有效的小说数据: $item');
// 返回一个错误占位小说对象
final now = DateTime.now();
return Novel(
id: 'error_${now.millisecondsSinceEpoch}',
title: '数据错误',
createdAt: now,
updatedAt: now,
acts: [],
);
}
}).toList();
} else if (processedData is Map<String, dynamic>) {
// 处理单个对象
return [_convertToNovelModel(processedData)];
}
return [];
}
/// 将WebFlux响应转换为单个Novel
Novel? _convertToSingleNovel(dynamic data) {
final processedData = _handleFluxResponse(data);
if (processedData == null) {
return null;
}
if (processedData is List && processedData.isNotEmpty) {
// 如果是列表,取第一个元素
final firstItem = processedData.first;
if (firstItem is Map<String, dynamic>) {
return _convertToNovelModel(firstItem);
}
} else if (processedData is Map<String, dynamic>) {
// 如果是单个对象,直接转换
return _convertToNovelModel(processedData);
}
return null;
}
/// 将后端Novel模型转换为前端Novel模型
Novel _convertToNovelModel(Map<String, dynamic> json) {
// 检查是否为NovelWithScenesDto格式
bool isNovelWithScenesDto =
json.containsKey('novel') && json.containsKey('scenesByChapter');
// 如果是NovelWithScenesDto格式提取novel部分
Map<String, dynamic> novelData =
isNovelWithScenesDto ? json['novel'] as Map<String, dynamic> : json;
Map<String, List<dynamic>>? scenesByChapter = isNovelWithScenesDto
? (json['scenesByChapter'] as Map<String, dynamic>)
.map((key, value) => MapEntry(key, value as List<dynamic>))
: null;
// 提取结构信息
final structure = novelData['structure'] as Map<String, dynamic>? ?? {};
final acts = (structure['acts'] as List?)?.map((actJson) {
final act = actJson as Map<String, dynamic>;
final chapters = (act['chapters'] as List?)?.map((chapterJson) {
final chapter = chapterJson as Map<String, dynamic>;
// 章节ID
final chapterId = chapter['id'];
// 获取场景ID列表
List<String> sceneIds = [];
if (chapter['sceneIds'] != null && chapter['sceneIds'] is List) {
//AppLogger.d('NovelRepositoryImpl', 'Found sceneIds in chapter ${chapter['id']}: ${chapter['sceneIds']}');
sceneIds = (chapter['sceneIds'] as List)
.map((id) => id.toString())
.toList();
//AppLogger.d('NovelRepositoryImpl', 'Parsed ${sceneIds.length} sceneIds');
} else {
//AppLogger.d('NovelRepositoryImpl', 'No sceneIds found in chapter ${chapter['id']}');
}
// 如果是NovelWithScenesDto格式且有该章节的场景数据添加场景
List<Scene> scenes = [];
if (isNovelWithScenesDto &&
scenesByChapter != null &&
scenesByChapter.containsKey(chapterId)) {
scenes = scenesByChapter[chapterId]!
.map((sceneJson) =>
Scene.fromJson(sceneJson as Map<String, dynamic>))
.toList();
}
return Chapter(
id: chapterId,
title: chapter['title'],
order: chapter['order'],
scenes: scenes,
sceneIds: sceneIds, // 添加场景ID列表
);
}).toList() ??
[];
return Act(
id: act['id'],
title: act['title'],
order: act['order'],
chapters: chapters,
);
}).toList() ??
[];
// 提取元数据
final metadata = novelData['metadata'] as Map<String, dynamic>? ?? {};
// 从元数据中获取字数和其他信息
final wordCount = metadata['wordCount'] as int? ?? 0;
final readTime = metadata['readTime'] as int? ?? 0;
final version = metadata['version'] as int? ?? 1;
final contributors = (metadata['contributors'] as List?)?.cast<String>() ?? <String>[];
// 解析创建时间和更新时间
DateTime createdAt;
DateTime updatedAt;
try {
// 使用新的工具函数解析 createdAt 和 updatedAt
createdAt = parseBackendDateTime(novelData['createdAt']);
updatedAt = parseBackendDateTime(novelData['updatedAt']);
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'解析小说时间戳失败',
e);
createdAt = DateTime.now();
updatedAt = DateTime.now();
}
// 创建Author对象
Author? author;
if (novelData['author'] != null) {
final authorData = novelData['author'] as Map<String, dynamic>;
author = Author(
id: authorData['id'] ?? '',
username: authorData['username'] ?? '未知作者',
);
}
return Novel(
id: novelData['id'],
title: novelData['title'] ?? '无标题',
coverUrl: novelData['coverImage'] ?? '',
createdAt: createdAt,
updatedAt: updatedAt,
acts: acts,
lastEditedChapterId: novelData['lastEditedChapterId'],
author: author,
wordCount: wordCount, // 使用从元数据提取的字数
readTime: readTime, // 使用从元数据提取的阅读时间
version: version, // 使用从元数据提取的版本号
contributors: contributors, // 使用从元数据提取的贡献者列表
);
}
/// 将后端Scene模型转换为前端Scene模型
Scene _convertToSceneModel(Map<String, dynamic> json) {
// 解析更新时间
DateTime lastEdited;
if (json.containsKey('updatedAt')) {
lastEdited = parseBackendDateTime(json['updatedAt']);
} else {
lastEdited = DateTime.now();
}
final sceneId =
json['id'] ?? 'scene_${DateTime.now().millisecondsSinceEpoch}';
return Scene(
id: sceneId,
content: json['content'] ?? '',
wordCount: json['wordCount'] ?? 0,
summary: Summary(
id: 'summary_$sceneId',
content: json['summary'] ?? '',
),
lastEdited: lastEdited,
);
}
/// 处理WebFlux流式响应数据统一处理数据类型
dynamic _handleFluxResponse(dynamic data) {
if (data == null) return null;
// 如果是列表类型确保列表中的每个元素都是Map类型
if (data is List) {
return data;
}
// 如果是Map类型直接返回
else if (data is Map<String, dynamic>) {
return data;
}
// 其他类型记录警告并返回null
else {
AppLogger.w(
'Services/api_service/repositories/impl/novel_repository_impl',
'警告API返回了意外的数据类型: ${data.runtimeType}');
return null;
}
}
/// 更新场景内容
@override
Future<Scene> updateSceneContent(String novelId, String actId,
String chapterId, String sceneId, Scene scene) async {
try {
// 将前端模型转换为后端模型
final sceneJson = {
'id': scene.id,
'novelId': novelId,
'chapterId': chapterId,
'content': scene.content,
'summary': scene.summary.content,
'wordCount': scene.wordCount,
'title': '场景 ${scene.id}', // 添加标题
};
// 使用新的API路径更新场景内容
final data = await _apiClient.updateScene(sceneJson);
return _convertToSceneModel(data);
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'更新场景内容失败',
e);
rethrow;
}
}
/// 更新摘要内容
@override
Future<Summary> updateSummary(String novelId, String actId, String chapterId,
String sceneId, Summary summary) async {
try {
// 获取当前场景
final scene = await fetchSceneContent(novelId, actId, chapterId, sceneId);
// 更新摘要
final updatedScene = await updateSceneContent(
novelId,
actId,
chapterId,
sceneId,
scene.copyWith(summary: summary),
);
return updatedScene.summary;
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'更新摘要失败',
e);
rethrow;
}
}
/// 获取当前章节后面指定数量的章节和场景内容
@override
Future<Novel?> fetchChaptersAfter(String novelId, String currentChapterId, {int chaptersLimit = 3, bool includeCurrentChapter = true}) async {
try {
AppLogger.i('NovelRepositoryImpl/fetchChaptersAfter',
'获取后续章节: novelId=$novelId, currentChapterId=$currentChapterId, limit=$chaptersLimit, includeCurrentChapter=$includeCurrentChapter');
// 调用新的API接口
final data = await _apiClient.getChaptersAfter(
novelId,
currentChapterId,
chaptersLimit: chaptersLimit,
includeCurrentChapter: includeCurrentChapter
);
if (data == null) {
AppLogger.w('NovelRepositoryImpl/fetchChaptersAfter', '后端返回空数据');
return null;
}
// 转换数据格式
final novel = _convertToNovelModel(data);
AppLogger.i('NovelRepositoryImpl/fetchChaptersAfter',
'获取后续章节成功: $novelId, 返回章节数: ${novel.acts.fold(0, (sum, act) => sum + act.chapters.length)}');
return novel;
} catch (e) {
AppLogger.e('NovelRepositoryImpl/fetchChaptersAfter',
'获取后续章节失败', e);
return null;
}
}
/// 获取指定章节后面的章节列表(用于预加载)
@override
Future<ChaptersForPreloadDto?> fetchChaptersForPreload(
String novelId,
String currentChapterId, {
int chaptersLimit = 3,
bool includeCurrentChapter = false,
}) async {
try {
AppLogger.i('NovelRepositoryImpl/fetchChaptersForPreload',
'获取章节列表用于预加载: novelId=$novelId, currentChapterId=$currentChapterId, chaptersLimit=$chaptersLimit, includeCurrentChapter=$includeCurrentChapter');
// 调用后端API
final requestData = {
'novelId': novelId,
'currentChapterId': currentChapterId,
'chaptersLimit': chaptersLimit,
'includeCurrentChapter': includeCurrentChapter,
};
final data = await _apiClient.post('/novels/get-chapters-for-preload', data: requestData);
if (data == null) {
AppLogger.w('NovelRepositoryImpl/fetchChaptersForPreload', '后端返回空数据');
return null;
}
// 将后端返回的数据转换为DTO
final dto = ChaptersForPreloadDto.fromJson(data);
AppLogger.i('NovelRepositoryImpl/fetchChaptersForPreload',
'成功获取章节列表用于预加载: novelId=$novelId, 章节数=${dto.chapterCount}, 场景章节数=${dto.scenesByChapter.keys.length}');
return dto;
} catch (e) {
AppLogger.e('NovelRepositoryImpl/fetchChaptersForPreload',
'获取章节列表用于预加载失败', e);
return null;
}
}
@override
Future<Novel> fetchNovelOnlyStructure(String id) {
// TODO: implement fetchNovelOnlyStructure
throw UnimplementedError();
}
@override
Future<Novel> fetchNovelText(String id) async {
try {
final data = await _apiClient.getNovelDetailByIdText(id);
final novel = _convertToSingleNovel(data);
if (novel == null) {
throw ApiException(404, '小说不存在或数据格式不正确');
}
return novel;
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/novel_repository_impl',
'获取小说详情失败',
e);
rethrow;
}
}
/// 获取用户编辑器设置
Future<EditorSettings> getUserEditorSettings(String userId) async {
try {
AppLogger.d('NovelRepositoryImpl', '获取用户编辑器设置: userId=$userId');
final data = await _apiClient.getUserEditorSettings(userId);
if (data != null && data is Map<String, dynamic>) {
final settings = EditorSettings.fromJson(data);
AppLogger.d('NovelRepositoryImpl', '成功获取用户编辑器设置');
return settings;
} else {
AppLogger.w('NovelRepositoryImpl', '获取到空的编辑器设置,使用默认设置');
return const EditorSettings();
}
} catch (e) {
AppLogger.e('NovelRepositoryImpl', '获取用户编辑器设置失败,使用默认设置', e);
return const EditorSettings();
}
}
/// 保存用户编辑器设置
Future<EditorSettings> saveUserEditorSettings(String userId, EditorSettings settings) async {
try {
AppLogger.d('NovelRepositoryImpl', '保存用户编辑器设置: userId=$userId');
final data = await _apiClient.saveUserEditorSettings(userId, settings.toJson());
if (data != null && data is Map<String, dynamic>) {
final savedSettings = EditorSettings.fromJson(data);
AppLogger.d('NovelRepositoryImpl', '成功保存用户编辑器设置');
return savedSettings;
} else {
AppLogger.w('NovelRepositoryImpl', '保存编辑器设置响应格式不正确,返回原设置');
return settings;
}
} catch (e) {
AppLogger.e('NovelRepositoryImpl', '保存用户编辑器设置失败', e);
throw ApiException(-1, '保存编辑器设置失败: $e');
}
}
/// 重置用户编辑器设置为默认值
Future<EditorSettings> resetUserEditorSettings(String userId) async {
try {
AppLogger.d('NovelRepositoryImpl', '重置用户编辑器设置: userId=$userId');
final data = await _apiClient.resetUserEditorSettings(userId);
if (data != null && data is Map<String, dynamic>) {
final resetSettings = EditorSettings.fromJson(data);
AppLogger.d('NovelRepositoryImpl', '成功重置用户编辑器设置');
return resetSettings;
} else {
AppLogger.w('NovelRepositoryImpl', '重置编辑器设置响应格式不正确,返回默认设置');
return const EditorSettings();
}
} catch (e) {
AppLogger.e('NovelRepositoryImpl', '重置用户编辑器设置失败', e);
throw ApiException(-1, '重置编辑器设置失败: $e');
}
}
}

View File

@@ -0,0 +1,519 @@
import 'dart:async';
import 'package:ainoval/models/novel_setting_item.dart';
import 'package:ainoval/models/setting_group.dart';
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/services/api_service/repositories/novel_setting_repository.dart';
import 'package:ainoval/utils/logger.dart';
/// 小说设定仓储实现
class NovelSettingRepositoryImpl implements NovelSettingRepository {
NovelSettingRepositoryImpl({required this.apiClient});
final ApiClient apiClient;
// API路径基础部分
String _getBasePath(String novelId) => '/novels/$novelId/settings';
// ==================== 设定条目管理 ====================
@override
Future<NovelSettingItem> createSettingItem({
required String novelId,
required NovelSettingItem settingItem,
}) async {
AppLogger.i('NovelSettingRepoImpl', '创建设定条目: novelId=$novelId, name=${settingItem.name}');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/items/create',
data: settingItem.toJson(),
);
final result = NovelSettingItem.fromJson(response);
AppLogger.i('NovelSettingRepoImpl', '创建设定条目成功: id=${result.id}');
return result;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '创建设定条目失败', e, stackTrace);
rethrow;
}
}
@override
Future<List<NovelSettingItem>> getNovelSettingItems({
required String novelId,
String? type,
String? name,
int? priority,
String? generatedBy,
String? status,
required int page,
required int size,
required String sortBy,
required String sortDirection,
}) async {
AppLogger.i('NovelSettingRepoImpl', '获取设定条目列表: novelId=$novelId');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/items/list',
data: {
'type': type,
'name': name,
'priority': priority,
'generatedBy': generatedBy,
'status': status,
'page': page,
'size': size,
'sortBy': sortBy,
'sortDirection': sortDirection,
},
);
final List<dynamic> itemsJson = response;
final items = itemsJson
.map((json) => NovelSettingItem.fromJson(json))
.toList();
AppLogger.i('NovelSettingRepoImpl', '获取设定条目列表成功: count=${items.length}');
return items;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '获取设定条目列表失败', e, stackTrace);
rethrow;
}
}
@override
Future<NovelSettingItem> getSettingItemDetail({
required String novelId,
required String itemId,
}) async {
AppLogger.i('NovelSettingRepoImpl', '获取设定条目详情: novelId=$novelId, itemId=$itemId');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/items/detail',
data: {
'itemId': itemId,
},
);
final result = NovelSettingItem.fromJson(response);
AppLogger.i('NovelSettingRepoImpl', '获取设定条目详情成功: id=${result.id}');
return result;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '获取设定条目详情失败', e, stackTrace);
rethrow;
}
}
@override
Future<NovelSettingItem> updateSettingItem({
required String novelId,
required String itemId,
required NovelSettingItem settingItem,
}) async {
AppLogger.i('NovelSettingRepoImpl', '更新设定条目: novelId=$novelId, itemId=$itemId');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/items/update',
data: {
'itemId': itemId,
'settingItem': settingItem.toJson(),
},
);
final result = NovelSettingItem.fromJson(response);
AppLogger.i('NovelSettingRepoImpl', '更新设定条目成功: id=${result.id}');
return result;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '更新设定条目失败', e, stackTrace);
rethrow;
}
}
@override
Future<void> deleteSettingItem({
required String novelId,
required String itemId,
}) async {
AppLogger.i('NovelSettingRepoImpl', '删除设定条目: novelId=$novelId, itemId=$itemId');
try {
await apiClient.post(
'${_getBasePath(novelId)}/items/delete',
data: {
'itemId': itemId,
},
);
AppLogger.i('NovelSettingRepoImpl', '删除设定条目成功');
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '删除设定条目失败', e, stackTrace);
rethrow;
}
}
@override
Future<NovelSettingItem> addSettingRelationship({
required String novelId,
required String itemId,
required String targetItemId,
required String relationshipType,
String? description,
}) async {
AppLogger.i('NovelSettingRepoImpl',
'添加设定关系: novelId=$novelId, itemId=$itemId, targetItemId=$targetItemId');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/items/add-relationship',
data: {
'itemId': itemId,
'targetItemId': targetItemId,
'relationshipType': relationshipType,
'description': description,
},
);
final result = NovelSettingItem.fromJson(response);
AppLogger.i('NovelSettingRepoImpl', '添加设定关系成功');
return result;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '添加设定关系失败', e, stackTrace);
rethrow;
}
}
@override
Future<void> removeSettingRelationship({
required String novelId,
required String itemId,
required String targetItemId,
required String relationshipType,
}) async {
AppLogger.i('NovelSettingRepoImpl',
'删除设定关系: novelId=$novelId, itemId=$itemId, targetItemId=$targetItemId');
try {
await apiClient.post(
'${_getBasePath(novelId)}/items/remove-relationship',
data: {
'itemId': itemId,
'targetItemId': targetItemId,
'relationshipType': relationshipType,
},
);
AppLogger.i('NovelSettingRepoImpl', '删除设定关系成功');
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '删除设定关系失败', e, stackTrace);
rethrow;
}
}
@override
Future<NovelSettingItem> setParentChildRelationship({
required String novelId,
required String childId,
required String parentId,
}) async {
AppLogger.i('NovelSettingRepoImpl', '设置父子关系: novelId=$novelId, childId=$childId, parentId=$parentId');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/items/set-parent',
data: {
'childId': childId,
'parentId': parentId,
'description': null,
},
);
final result = NovelSettingItem.fromJson(response);
AppLogger.i('NovelSettingRepoImpl', '设置父子关系成功: id=${result.id}');
return result;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '设置父子关系失败', e, stackTrace);
rethrow;
}
}
@override
Future<NovelSettingItem> removeParentChildRelationship({
required String novelId,
required String childId,
}) async {
AppLogger.i('NovelSettingRepoImpl', '移除父子关系: novelId=$novelId, childId=$childId');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/items/remove-parent',
data: {
'childId': childId,
'parentId': null,
'description': null,
},
);
final result = NovelSettingItem.fromJson(response);
AppLogger.i('NovelSettingRepoImpl', '移除父子关系成功: id=${result.id}');
return result;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '移除父子关系失败', e, stackTrace);
rethrow;
}
}
// ==================== 设定组管理 ====================
@override
Future<SettingGroup> createSettingGroup({
required String novelId,
required SettingGroup settingGroup,
}) async {
AppLogger.i('NovelSettingRepoImpl', '创建设定组: novelId=$novelId, name=${settingGroup.name}');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/groups/create',
data: settingGroup.toJson(),
);
final result = SettingGroup.fromJson(response);
AppLogger.i('NovelSettingRepoImpl', '创建设定组成功: id=${result.id}');
return result;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '创建设定组失败', e, stackTrace);
rethrow;
}
}
@override
Future<List<SettingGroup>> getNovelSettingGroups({
required String novelId,
String? name,
bool? isActiveContext,
}) async {
AppLogger.i('NovelSettingRepoImpl', '获取设定组列表: novelId=$novelId');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/groups/list',
data: {
'name': name,
'isActiveContext': isActiveContext,
},
);
final List<dynamic> groupsJson = response;
final groups = groupsJson
.map((json) => SettingGroup.fromJson(json))
.toList();
AppLogger.i('NovelSettingRepoImpl', '获取设定组列表成功: count=${groups.length}');
return groups;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '获取设定组列表失败', e, stackTrace);
rethrow;
}
}
@override
Future<SettingGroup> getSettingGroupDetail({
required String novelId,
required String groupId,
}) async {
AppLogger.i('NovelSettingRepoImpl', '获取设定组详情: novelId=$novelId, groupId=$groupId');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/groups/detail',
data: {
'groupId': groupId,
},
);
final result = SettingGroup.fromJson(response);
AppLogger.i('NovelSettingRepoImpl', '获取设定组详情成功: id=${result.id}');
return result;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '获取设定组详情失败', e, stackTrace);
rethrow;
}
}
@override
Future<SettingGroup> updateSettingGroup({
required String novelId,
required String groupId,
required SettingGroup settingGroup,
}) async {
AppLogger.i('NovelSettingRepoImpl', '更新设定组: novelId=$novelId, groupId=$groupId');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/groups/update',
data: {
'groupId': groupId,
'settingGroup': settingGroup.toJson(),
},
);
final result = SettingGroup.fromJson(response);
AppLogger.i('NovelSettingRepoImpl', '更新设定组成功: id=${result.id}');
return result;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '更新设定组失败', e, stackTrace);
rethrow;
}
}
@override
Future<void> deleteSettingGroup({
required String novelId,
required String groupId,
}) async {
AppLogger.i('NovelSettingRepoImpl', '删除设定组: novelId=$novelId, groupId=$groupId');
try {
await apiClient.post(
'${_getBasePath(novelId)}/groups/delete',
data: {
'groupId': groupId,
},
);
AppLogger.i('NovelSettingRepoImpl', '删除设定组成功');
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '删除设定组失败', e, stackTrace);
rethrow;
}
}
@override
Future<SettingGroup> addItemToGroup({
required String novelId,
required String groupId,
required String itemId,
}) async {
AppLogger.i('NovelSettingRepoImpl',
'添加条目到设定组: novelId=$novelId, groupId=$groupId, itemId=$itemId');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/groups/add-item',
data: {
'groupId': groupId,
'itemId': itemId,
},
);
final result = SettingGroup.fromJson(response);
AppLogger.i('NovelSettingRepoImpl', '添加条目到设定组成功');
return result;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '添加条目到设定组失败', e, stackTrace);
rethrow;
}
}
@override
Future<void> removeItemFromGroup({
required String novelId,
required String groupId,
required String itemId,
}) async {
AppLogger.i('NovelSettingRepoImpl',
'从设定组移除条目: novelId=$novelId, groupId=$groupId, itemId=$itemId');
try {
await apiClient.post(
'${_getBasePath(novelId)}/groups/remove-item',
data: {
'groupId': groupId,
'itemId': itemId,
},
);
AppLogger.i('NovelSettingRepoImpl', '从设定组移除条目成功');
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '从设定组移除条目失败', e, stackTrace);
rethrow;
}
}
@override
Future<SettingGroup> setGroupActiveContext({
required String novelId,
required String groupId,
required bool isActive,
}) async {
AppLogger.i('NovelSettingRepoImpl',
'设置设定组激活状态: novelId=$novelId, groupId=$groupId, isActive=$isActive');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/groups/set-active',
data: {
'groupId': groupId,
'active': isActive,
},
);
final result = SettingGroup.fromJson(response);
AppLogger.i('NovelSettingRepoImpl', '设置设定组激活状态成功');
return result;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '设置设定组激活状态失败', e, stackTrace);
rethrow;
}
}
// ==================== 高级功能 ====================
@override
Future<List<NovelSettingItem>> extractSettingsFromText({
required String novelId,
required String text,
required String type,
}) async {
AppLogger.i('NovelSettingRepoImpl', '从文本提取设定: novelId=$novelId, type=$type');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/extract',
data: {
'text': text,
'type': type,
},
);
final List<dynamic> itemsJson = response;
final items = itemsJson
.map((json) => NovelSettingItem.fromJson(json))
.toList();
AppLogger.i('NovelSettingRepoImpl', '从文本提取设定成功: count=${items.length}');
return items;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '从文本提取设定失败', e, stackTrace);
rethrow;
}
}
@override
Future<List<NovelSettingItem>> searchSettingItems({
required String novelId,
required String query,
List<String>? types,
List<String>? groupIds,
double? minScore,
int? maxResults,
}) async {
AppLogger.i('NovelSettingRepoImpl', '搜索设定条目: novelId=$novelId, query=$query');
try {
final response = await apiClient.post(
'${_getBasePath(novelId)}/search',
data: {
'query': query,
'types': types,
'groupIds': groupIds,
'minScore': minScore,
'maxResults': maxResults,
},
);
final List<dynamic> itemsJson = response;
final items = itemsJson
.map((json) => NovelSettingItem.fromJson(json))
.toList();
AppLogger.i('NovelSettingRepoImpl', '搜索设定条目成功: count=${items.length}');
return items;
} catch (e, stackTrace) {
AppLogger.e('NovelSettingRepoImpl', '搜索设定条目失败', e, stackTrace);
rethrow;
}
}
}

View File

@@ -0,0 +1,253 @@
import 'package:ainoval/models/novel_snippet.dart';
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/services/api_service/repositories/novel_snippet_repository.dart';
import 'package:ainoval/utils/logger.dart';
/// 小说片段仓储实现类
///
/// 使用ApiClient调用后端API实现片段相关操作
class NovelSnippetRepositoryImpl implements NovelSnippetRepository {
final ApiClient _apiClient;
NovelSnippetRepositoryImpl(this._apiClient);
@override
Future<NovelSnippet> createSnippet(CreateSnippetRequest request) async {
try {
AppLogger.i('NovelSnippetRepository', '创建片段: ${request.title}');
final response = await _apiClient.createSnippet(request.toJson());
if (response is Map<String, dynamic>) {
return NovelSnippet.fromJson(response);
} else {
throw Exception('创建片段响应格式错误: $response');
}
} catch (e) {
AppLogger.e('NovelSnippetRepository', '创建片段失败', e);
rethrow;
}
}
@override
Future<SnippetPageResult<NovelSnippet>> getSnippetsByNovelId(
String novelId, {
int page = 0,
int size = 20,
}) async {
try {
AppLogger.d('NovelSnippetRepository', '获取小说片段列表: novelId=$novelId, page=$page, size=$size');
final response = await _apiClient.getSnippetsByNovelId(novelId, page: page, size: size);
if (response is Map<String, dynamic>) {
return SnippetPageResult.fromJson(
response,
(json) => NovelSnippet.fromJson(json as Map<String, dynamic>),
);
} else {
throw Exception('获取片段列表响应格式错误: $response');
}
} catch (e) {
AppLogger.e('NovelSnippetRepository', '获取小说片段列表失败: novelId=$novelId', e);
rethrow;
}
}
@override
Future<NovelSnippet> getSnippetDetail(String snippetId) async {
try {
AppLogger.d('NovelSnippetRepository', '获取片段详情: snippetId=$snippetId');
final response = await _apiClient.getSnippetDetail(snippetId);
if (response is Map<String, dynamic>) {
return NovelSnippet.fromJson(response);
} else {
throw Exception('获取片段详情响应格式错误: $response');
}
} catch (e) {
AppLogger.e('NovelSnippetRepository', '获取片段详情失败: snippetId=$snippetId', e);
rethrow;
}
}
@override
Future<NovelSnippet> updateSnippetContent(UpdateSnippetContentRequest request) async {
try {
AppLogger.i('NovelSnippetRepository', '更新片段内容: snippetId=${request.snippetId}');
final response = await _apiClient.updateSnippetContent(request.toJson());
if (response is Map<String, dynamic>) {
return NovelSnippet.fromJson(response);
} else {
throw Exception('更新片段内容响应格式错误: $response');
}
} catch (e) {
AppLogger.e('NovelSnippetRepository', '更新片段内容失败: snippetId=${request.snippetId}', e);
rethrow;
}
}
@override
Future<NovelSnippet> updateSnippetTitle(UpdateSnippetTitleRequest request) async {
try {
AppLogger.i('NovelSnippetRepository', '更新片段标题: snippetId=${request.snippetId}');
final response = await _apiClient.updateSnippetTitle(request.toJson());
if (response is Map<String, dynamic>) {
return NovelSnippet.fromJson(response);
} else {
throw Exception('更新片段标题响应格式错误: $response');
}
} catch (e) {
AppLogger.e('NovelSnippetRepository', '更新片段标题失败: snippetId=${request.snippetId}', e);
rethrow;
}
}
@override
Future<NovelSnippet> updateSnippetFavorite(UpdateSnippetFavoriteRequest request) async {
try {
AppLogger.i('NovelSnippetRepository', '更新片段收藏状态: snippetId=${request.snippetId}, isFavorite=${request.isFavorite}');
final response = await _apiClient.updateSnippetFavorite(request.toJson());
if (response is Map<String, dynamic>) {
return NovelSnippet.fromJson(response);
} else {
throw Exception('更新片段收藏状态响应格式错误: $response');
}
} catch (e) {
AppLogger.e('NovelSnippetRepository', '更新片段收藏状态失败: snippetId=${request.snippetId}', e);
rethrow;
}
}
@override
Future<SnippetPageResult<NovelSnippetHistory>> getSnippetHistory(
String snippetId, {
int page = 0,
int size = 10,
}) async {
try {
AppLogger.d('NovelSnippetRepository', '获取片段历史记录: snippetId=$snippetId, page=$page, size=$size');
final response = await _apiClient.getSnippetHistory(snippetId, page: page, size: size);
if (response is Map<String, dynamic>) {
return SnippetPageResult.fromJson(
response,
(json) => NovelSnippetHistory.fromJson(json as Map<String, dynamic>),
);
} else {
throw Exception('获取片段历史记录响应格式错误: $response');
}
} catch (e) {
AppLogger.e('NovelSnippetRepository', '获取片段历史记录失败: snippetId=$snippetId', e);
rethrow;
}
}
@override
Future<NovelSnippetHistory> previewHistoryVersion(String snippetId, int version) async {
try {
AppLogger.d('NovelSnippetRepository', '预览历史版本: snippetId=$snippetId, version=$version');
final response = await _apiClient.previewSnippetHistoryVersion(snippetId, version);
if (response is Map<String, dynamic>) {
return NovelSnippetHistory.fromJson(response);
} else {
throw Exception('预览历史版本响应格式错误: $response');
}
} catch (e) {
AppLogger.e('NovelSnippetRepository', '预览历史版本失败: snippetId=$snippetId, version=$version', e);
rethrow;
}
}
@override
Future<NovelSnippet> revertToHistoryVersion(RevertSnippetVersionRequest request) async {
try {
AppLogger.i('NovelSnippetRepository', '回退到历史版本: snippetId=${request.snippetId}, version=${request.version}');
final response = await _apiClient.revertSnippetToVersion(request.toJson());
if (response is Map<String, dynamic>) {
return NovelSnippet.fromJson(response);
} else {
throw Exception('回退到历史版本响应格式错误: $response');
}
} catch (e) {
AppLogger.e('NovelSnippetRepository', '回退到历史版本失败: snippetId=${request.snippetId}, version=${request.version}', e);
rethrow;
}
}
@override
Future<void> deleteSnippet(String snippetId) async {
try {
AppLogger.i('NovelSnippetRepository', '删除片段: snippetId=$snippetId');
await _apiClient.deleteSnippet(snippetId);
AppLogger.i('NovelSnippetRepository', '片段删除成功: snippetId=$snippetId');
} catch (e) {
AppLogger.e('NovelSnippetRepository', '删除片段失败: snippetId=$snippetId', e);
rethrow;
}
}
@override
Future<SnippetPageResult<NovelSnippet>> getFavoriteSnippets({
int page = 0,
int size = 20,
}) async {
try {
AppLogger.d('NovelSnippetRepository', '获取收藏片段: page=$page, size=$size');
final response = await _apiClient.getFavoriteSnippets(page: page, size: size);
if (response is Map<String, dynamic>) {
return SnippetPageResult.fromJson(
response,
(json) => NovelSnippet.fromJson(json as Map<String, dynamic>),
);
} else {
throw Exception('获取收藏片段响应格式错误: $response');
}
} catch (e) {
AppLogger.e('NovelSnippetRepository', '获取收藏片段失败', e);
rethrow;
}
}
@override
Future<SnippetPageResult<NovelSnippet>> searchSnippets(
String novelId,
String searchText, {
int page = 0,
int size = 20,
}) async {
try {
AppLogger.d('NovelSnippetRepository', '搜索片段: novelId=$novelId, searchText=$searchText, page=$page, size=$size');
final response = await _apiClient.searchSnippets(novelId, searchText, page: page, size: size);
if (response is Map<String, dynamic>) {
return SnippetPageResult.fromJson(
response,
(json) => NovelSnippet.fromJson(json as Map<String, dynamic>),
);
} else {
throw Exception('搜索片段响应格式错误: $response');
}
} catch (e) {
AppLogger.e('NovelSnippetRepository', '搜索片段失败: novelId=$novelId, searchText=$searchText', e);
rethrow;
}
}
}

View File

@@ -0,0 +1,262 @@
import 'package:ainoval/models/preset_models.dart';
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/services/api_service/repositories/preset_aggregation_repository.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:dio/dio.dart';
/// 预设聚合仓储实现
class PresetAggregationRepositoryImpl implements PresetAggregationRepository {
final ApiClient _apiClient;
static const String _baseUrl = '/preset-aggregation';
static const String _tag = 'PresetAggregationRepositoryImpl';
/// 构造函数
PresetAggregationRepositoryImpl(this._apiClient);
@override
Future<PresetPackage> getCompletePresetPackage(
String featureType, {
String? novelId,
}) async {
try {
final Map<String, dynamic> queryParams = {
'featureType': featureType,
};
if (novelId != null) {
queryParams['novelId'] = novelId;
}
// 构建查询字符串
String url = '$_baseUrl/package';
if (queryParams.isNotEmpty) {
final queryString = queryParams.entries
.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value.toString())}')
.join('&');
url = '$url?$queryString';
}
final result = await _apiClient.get(url);
return PresetPackage.fromJson(result);
} catch (e) {
AppLogger.e(_tag, '获取完整预设包失败: featureType=$featureType, novelId=$novelId', e);
// 返回空的预设包作为降级处理
return PresetPackage(
featureType: featureType,
systemPresets: [],
userPresets: [],
favoritePresets: [],
quickAccessPresets: [],
recentlyUsedPresets: [],
totalCount: 0,
cachedAt: DateTime.now(),
);
}
}
@override
Future<UserPresetOverview> getUserPresetOverview() async {
try {
final result = await _apiClient.get('$_baseUrl/overview');
return UserPresetOverview.fromJson(result);
} catch (e) {
AppLogger.e(_tag, '获取用户预设概览失败', e);
// 返回空的概览作为降级处理
return UserPresetOverview(
totalPresets: 0,
systemPresets: 0,
userPresets: 0,
favoritePresets: 0,
presetsByFeatureType: {},
recentFeatureTypes: [],
popularTags: [],
generatedAt: DateTime.now(),
);
}
}
@override
Future<Map<String, PresetPackage>> getBatchPresetPackages({
List<String>? featureTypes,
String? novelId,
}) async {
try {
final Map<String, dynamic> queryParams = {};
if (featureTypes != null && featureTypes.isNotEmpty) {
queryParams['featureTypes'] = featureTypes.join(',');
}
if (novelId != null) {
queryParams['novelId'] = novelId;
}
// 构建查询字符串
String url = '$_baseUrl/batch';
if (queryParams.isNotEmpty) {
final queryString = queryParams.entries
.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value.toString())}')
.join('&');
url = '$url?$queryString';
}
final result = await _apiClient.get(url);
final Map<String, PresetPackage> packages = {};
if (result is Map<String, dynamic>) {
result.forEach((key, value) {
try {
packages[key] = PresetPackage.fromJson(value);
} catch (e) {
AppLogger.w(_tag, '解析预设包失败: $key', e);
}
});
}
return packages;
} catch (e) {
AppLogger.e(_tag, '批量获取预设包失败: featureTypes=$featureTypes, novelId=$novelId', e);
return {};
}
}
@override
Future<CacheWarmupResult> warmupCache() async {
try {
final result = await _apiClient.post('$_baseUrl/warmup', data: {});
return CacheWarmupResult.fromJson(result);
} catch (e) {
AppLogger.e(_tag, '预热缓存失败', e);
return CacheWarmupResult(
success: false,
warmedFeatureTypes: 0,
warmedPresets: 0,
durationMs: 0,
errorMessage: e.toString(),
);
}
}
@override
Future<AggregationCacheStats> getCacheStats() async {
try {
final result = await _apiClient.get('$_baseUrl/cache/stats');
return AggregationCacheStats.fromJson(result);
} catch (e) {
AppLogger.e(_tag, '获取缓存统计失败', e);
return AggregationCacheStats(
hitRate: 0.0,
cacheEntries: 0,
cacheSizeBytes: 0,
lastUpdated: DateTime.now(),
);
}
}
@override
Future<String> clearCache() async {
try {
final result = await _apiClient.delete('$_baseUrl/cache');
if (result is Map<String, dynamic> && result.containsKey('message')) {
return result['message'] as String;
}
return '缓存清除成功';
} catch (e) {
AppLogger.e(_tag, '清除缓存失败', e);
throw Exception('清除缓存失败: ${e.toString()}');
}
}
@override
Future<Map<String, dynamic>> healthCheck() async {
try {
final result = await _apiClient.get('$_baseUrl/health');
if (result is Map<String, dynamic>) {
return result;
}
return {'status': 'unknown'};
} catch (e) {
AppLogger.e(_tag, '聚合服务健康检查失败', e);
return {
'status': 'error',
'error': e.toString(),
'timestamp': DateTime.now().toIso8601String(),
};
}
}
@override
Future<AllUserPresetData> getAllUserPresetData({String? novelId}) async {
try {
final Map<String, dynamic> queryParams = {};
if (novelId != null) {
queryParams['novelId'] = novelId;
}
// 构建查询字符串
String url = '$_baseUrl/all-data';
if (queryParams.isNotEmpty) {
final queryString = queryParams.entries
.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value.toString())}')
.join('&');
url = '$url?$queryString';
}
AppLogger.i(_tag, '🚀 请求所有预设聚合数据: url=$url');
final result = await _apiClient.get(url);
// 检查响应格式 - API返回的是标准响应格式 {success, message, data}
if (result is! Map<String, dynamic>) {
throw Exception('响应格式错误: 不是JSON对象');
}
final response = result as Map<String, dynamic>;
AppLogger.i(_tag, '📋 响应字段: ${response.keys.toList()}');
if (response['success'] != true) {
throw Exception('请求失败: ${response['message'] ?? '未知错误'}');
}
final data = response['data'];
if (data == null) {
throw Exception('响应数据为空');
}
AppLogger.i(_tag, '✅ 开始解析聚合数据...');
final allData = AllUserPresetData.fromJson(data);
AppLogger.i(_tag, '✅ 所有预设聚合数据获取成功');
AppLogger.i(_tag, '📊 数据统计: 系统预设${allData.systemPresets.length}个, 用户预设分组${allData.userPresetsByFeatureType.length}个, 收藏${allData.favoritePresets.length}');
return allData;
} catch (e) {
AppLogger.e(_tag, '❌ 获取所有预设聚合数据失败: novelId=$novelId', e);
// 返回空的聚合数据作为降级处理
return AllUserPresetData(
userId: '',
overview: UserPresetOverview(
totalPresets: 0,
systemPresets: 0,
userPresets: 0,
favoritePresets: 0,
presetsByFeatureType: {},
recentFeatureTypes: [],
popularTags: [],
generatedAt: DateTime.now(),
),
packagesByFeatureType: {},
systemPresets: [],
userPresetsByFeatureType: {},
favoritePresets: [],
quickAccessPresets: [],
recentlyUsedPresets: [],
timestamp: DateTime.now(),
cacheDuration: 0,
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
import '../../../../models/public_model_config.dart';
import '../../../../utils/logger.dart';
import '../../base/api_client.dart';
import '../public_model_repository.dart';
/// 公共模型仓库实现
class PublicModelRepositoryImpl implements PublicModelRepository {
final ApiClient _apiClient;
static const String _tag = 'PublicModelRepositoryImpl';
PublicModelRepositoryImpl({required ApiClient apiClient}) : _apiClient = apiClient;
@override
Future<List<PublicModel>> getPublicModels() async {
try {
AppLogger.i(_tag, '获取公共模型列表');
final rawList = await _apiClient.getPublicModels();
final models = rawList.map((json) {
try {
return PublicModel.fromJson(json);
} catch (e) {
AppLogger.e(_tag, '解析公共模型数据失败', e);
AppLogger.d(_tag, '问题数据: $json');
// 跳过解析失败的模型,继续处理其他模型
return null;
}
}).whereType<PublicModel>().toList();
AppLogger.i(_tag, '获取公共模型列表成功: 共${models.length}个模型');
return models;
} catch (e, stackTrace) {
AppLogger.e(_tag, '获取公共模型列表失败', e, stackTrace);
rethrow;
}
}
}

View File

@@ -0,0 +1,237 @@
import 'dart:typed_data';
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/services/api_service/base/api_exception.dart';
import 'package:ainoval/services/api_service/repositories/storage_repository.dart';
import 'package:ainoval/services/api_service/repositories/impl/aliyun_oss_storage_repository.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:http/http.dart' as http;
import 'package:mime/mime.dart';
/// 默认存储库实现
class StorageRepositoryImpl implements StorageRepository {
final ApiClient _apiClient;
StorageRepositoryImpl(this._apiClient);
@override
Future<Map<String, dynamic>> getCoverUploadCredential({
required String novelId,
required String fileName,
String? contentType,
}) async {
try {
// 获取MIME类型如果未提供
final String mimeType = contentType ?? _getMimeType(fileName);
// 调用后端API获取上传凭证
// ApiClient现在已经在内部处理fileName和contentType参数
final credential = await _apiClient.getCoverUploadCredential(novelId);
if (credential is! Map<String, dynamic>) {
throw ApiException(-1, '获取上传凭证失败:返回类型错误');
}
// 添加额外信息到凭证中(如果不存在)
if (!credential.containsKey('contentType')) {
credential['contentType'] = mimeType;
}
if (!credential.containsKey('fileName')) {
credential['fileName'] = fileName;
}
// 记录结果,以便于调试
AppLogger.d(
'Services/api_service/repositories/impl/storage_repository_impl',
'获取上传凭证成功:包含字段 ${credential.keys.join(', ')}',
);
return credential;
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/storage_repository_impl',
'获取上传凭证失败',
e,
);
throw ApiException(-1, '获取上传凭证失败: $e');
}
}
@override
Future<String> uploadCoverImage({
required String novelId,
required Uint8List fileBytes,
required String fileName,
String? contentType,
bool updateNovelCover = true,
}) async {
try {
// 获取上传凭证
final credential = await getCoverUploadCredential(
novelId: novelId,
fileName: fileName,
contentType: contentType,
);
// === 识别阿里云 OSS 上传场景 ===
// 1) host 以 oss:// 开头
// 2) 或者 host 域名包含 aliyuncs.com典型形如 https://bucket.oss-cn-xx.aliyuncs.com
if (credential.containsKey('host')) {
final String hostStr = credential['host'].toString();
final bool isAliyunHost = hostStr.startsWith('oss://') || hostStr.contains('aliyuncs.com');
if (isAliyunHost) {
AppLogger.d(
'Services/api_service/repositories/impl/storage_repository_impl',
'检测到阿里云OSS URL切换到专用处理方式',
);
final ossSr = AliyunOssStorageRepository(_apiClient);
return await ossSr.uploadCoverImage(
novelId: novelId,
fileBytes: fileBytes,
fileName: fileName,
contentType: contentType,
updateNovelCover: updateNovelCover,
);
}
}
// 检查必要参数 - 处理阿里云OSS凭证
if (credential.containsKey('host') &&
credential.containsKey('key') &&
credential.containsKey('policy') &&
credential.containsKey('signature') &&
credential.containsKey('accessKeyId')) {
// 阿里云OSS上传
final uri = Uri.parse(credential['host']);
final request = http.MultipartRequest('POST', uri);
// 添加OSS表单字段
request.fields['key'] = credential['key'];
request.fields['policy'] = credential['policy'];
request.fields['signature'] = credential['signature'];
request.fields['OSSAccessKeyId'] = credential['accessKeyId'];
request.fields['success_action_status'] = '200';
// 如果有内容类型,添加到表单中
if (credential.containsKey('contentType')) {
request.fields['Content-Type'] = credential['contentType'];
}
// 添加文件
final mimeType = contentType ?? _getMimeType(fileName);
request.files.add(http.MultipartFile.fromBytes(
'file',
fileBytes,
filename: fileName,
contentType: mimeType.isNotEmpty ? null : null,
));
// 发送请求
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
// 检查响应
if (response.statusCode < 200 || response.statusCode >= 300) {
throw ApiException(response.statusCode, '上传失败: ${response.body}');
}
// 构建文件URL并返回
final fileUrl = '${credential['host']}/${credential['key']}';
// 通知后端上传完成更新小说封面URL可禁用
if (updateNovelCover) {
await _apiClient.updateNovelCover(novelId, fileUrl);
}
return fileUrl;
}
// 原来的通用上传实现
else if (credential.containsKey('uploadUrl') && credential.containsKey('formFields')) {
// 原有的通用上传逻辑
final uri = Uri.parse(credential['uploadUrl']);
final request = http.MultipartRequest('POST', uri);
// 添加表单字段
final formFields = credential['formFields'] as Map<String, dynamic>;
formFields.forEach((key, value) {
request.fields[key] = value.toString();
});
// 添加文件
final mimeType = contentType ?? _getMimeType(fileName);
request.files.add(http.MultipartFile.fromBytes(
'file',
fileBytes,
filename: fileName,
contentType: mimeType.isNotEmpty ? null : null,
));
// 发送请求
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
// 检查响应
if (response.statusCode < 200 || response.statusCode >= 300) {
throw ApiException(response.statusCode, '上传失败: ${response.body}');
}
// 获取文件URL
String fileUrl = '';
if (credential.containsKey('fileUrl')) {
fileUrl = credential['fileUrl'];
} else {
// 从响应中解析URL
try {
final responseData = Uri.parse(response.body);
fileUrl = responseData.toString();
} catch (e) {
// 如果无法解析响应使用预定的URL格式
fileUrl = '${credential['baseUrl']}/${credential['key']}';
}
}
// 通知后端上传完成更新小说封面URL可禁用
if (updateNovelCover) {
await _apiClient.updateNovelCover(novelId, fileUrl);
}
return fileUrl;
} else {
throw ApiException(-1, '上传凭证格式不支持: ${credential.keys.join(', ')}');
}
} catch (e) {
AppLogger.e(
'Services/api_service/repositories/impl/storage_repository_impl',
'上传封面图片失败',
e,
);
throw ApiException(-1, '上传封面图片失败: $e');
}
}
@override
Future<String> getFileAccessUrl({
required String fileKey,
int? expirationSeconds,
}) async {
// 对于公开读权限的文件直接返回URL
return fileKey;
}
@override
Future<bool> hasValidStorageConfig() async {
try {
// 尝试获取测试小说的上传凭证,如果成功则认为配置有效
await _apiClient.getCoverUploadCredential('test');
return true;
} catch (e) {
return false;
}
}
/// 根据文件名获取MIME类型
String _getMimeType(String fileName) {
final mimeType = lookupMimeType(fileName);
return mimeType ?? 'application/octet-stream';
}
}

View File

@@ -0,0 +1,238 @@
import '../../../../models/admin/subscription_models.dart';
import '../../../../utils/logger.dart';
import '../../base/api_client.dart';
import '../../base/api_exception.dart';
import '../subscription_repository.dart';
/// 订阅管理仓库实现
class SubscriptionRepositoryImpl implements SubscriptionRepository {
final ApiClient _apiClient;
static const String _tag = 'SubscriptionRepository';
SubscriptionRepositoryImpl({required ApiClient apiClient}) : _apiClient = apiClient;
@override
Future<List<SubscriptionPlan>> getAllPlans() async {
try {
AppLogger.d(_tag, '🔍 获取所有订阅计划');
final response = await _apiClient.get('/admin/subscription-plans');
// 添加详细的响应调试日志
AppLogger.d(_tag, '📡 订阅计划原始响应类型: ${response.runtimeType}');
AppLogger.d(_tag, '📡 订阅计划原始响应内容: $response');
// 解析响应数据
dynamic rawData;
if (response is Map<String, dynamic>) {
AppLogger.d(_tag, '📄 订阅计划响应是Map包含的键: ${response.keys.toList()}');
if (response.containsKey('data')) {
rawData = response['data'];
AppLogger.d(_tag, '📄 订阅计划data字段类型: ${rawData.runtimeType}');
AppLogger.d(_tag, '📄 订阅计划data字段内容: $rawData');
} else if (response.containsKey('success') && response['success'] == true) {
rawData = response['data'] ?? response;
AppLogger.d(_tag, '📄 订阅计划success结构提取的数据类型: ${rawData.runtimeType}');
} else {
rawData = response;
AppLogger.d(_tag, '📄 订阅计划直接使用整个response');
}
} else {
rawData = response;
AppLogger.d(_tag, '📄 订阅计划响应不是Map直接使用');
}
// 检查数据类型并转换为List兼容 List 与 {data: List} 两种结构)
List<dynamic> data;
if (rawData is List) {
data = rawData;
AppLogger.d(_tag, '✅ 订阅计划成功获得List长度: ${data.length}');
} else if (rawData is Map<String, dynamic>) {
AppLogger.d(_tag, '📄 订阅计划rawData是Map包含的键: ${rawData.keys.toList()}');
if (rawData.containsKey('content')) {
data = (rawData['content'] as List?) ?? [];
AppLogger.d(_tag, '✅ 订阅计划从content字段获得List长度: ${data.length}');
} else if (rawData.containsKey('data') && rawData['data'] is List) {
data = (rawData['data'] as List);
AppLogger.d(_tag, '✅ 订阅计划从data字段获得List长度: ${data.length}');
} else {
// 尝试将 Map 视为单个对象列表(极端兼容)
AppLogger.w(_tag, '⚠️ 订阅计划Map中未发现content/data列表字段返回空列表');
data = [];
}
} else {
AppLogger.e(_tag, '❌ 订阅计划无法识别的数据类型: ${rawData.runtimeType}');
throw ApiException(-1, '订阅计划数据格式错误: 未知的数据类型 ${rawData.runtimeType}');
}
AppLogger.d(_tag, '✅ 获取订阅计划成功: count=${data.length}');
return data.map((json) => SubscriptionPlan.fromJson(json as Map<String, dynamic>)).toList();
} catch (e) {
AppLogger.e(_tag, '❌ 获取订阅计划失败', e);
rethrow;
}
}
@override
Future<SubscriptionPlan> getPlanById(String id) async {
try {
AppLogger.d(_tag, '🔍 获取订阅计划详情: id=$id');
final response = await _apiClient.get('/admin/subscription-plans/$id');
dynamic planData;
if (response is Map<String, dynamic> && response.containsKey('data')) {
planData = response['data'];
} else if (response is Map<String, dynamic>) {
planData = response;
} else {
throw ApiException(-1, '订阅计划详情数据格式错误');
}
AppLogger.d(_tag, '✅ 获取订阅计划详情成功: id=$id');
return SubscriptionPlan.fromJson(planData as Map<String, dynamic>);
} catch (e) {
AppLogger.e(_tag, '❌ 获取订阅计划详情失败', e);
rethrow;
}
}
@override
Future<SubscriptionPlan> createPlan(SubscriptionPlan plan) async {
try {
AppLogger.d(_tag, '📝 创建订阅计划: ${plan.planName}');
final response = await _apiClient.post('/admin/subscription-plans', data: plan.toJson());
dynamic planData;
if (response is Map<String, dynamic> && response.containsKey('data')) {
planData = response['data'];
} else if (response is Map<String, dynamic>) {
planData = response;
} else {
throw ApiException(-1, '创建订阅计划响应格式错误');
}
AppLogger.d(_tag, '✅ 创建订阅计划成功: ${plan.planName}');
return SubscriptionPlan.fromJson(planData as Map<String, dynamic>);
} catch (e) {
AppLogger.e(_tag, '❌ 创建订阅计划失败', e);
rethrow;
}
}
@override
Future<SubscriptionPlan> updatePlan(String id, SubscriptionPlan plan) async {
try {
AppLogger.d(_tag, '📝 更新订阅计划: id=$id');
final response = await _apiClient.put('/admin/subscription-plans/$id', data: plan.toJson());
dynamic planData;
if (response is Map<String, dynamic> && response.containsKey('data')) {
planData = response['data'];
} else if (response is Map<String, dynamic>) {
planData = response;
} else {
throw ApiException(-1, '更新订阅计划响应格式错误');
}
AppLogger.d(_tag, '✅ 更新订阅计划成功: id=$id');
return SubscriptionPlan.fromJson(planData as Map<String, dynamic>);
} catch (e) {
AppLogger.e(_tag, '❌ 更新订阅计划失败', e);
rethrow;
}
}
@override
Future<void> deletePlan(String id) async {
try {
AppLogger.d(_tag, '🗑️ 删除订阅计划: id=$id');
await _apiClient.delete('/admin/subscription-plans/$id');
AppLogger.d(_tag, '✅ 删除订阅计划成功: id=$id');
} catch (e) {
AppLogger.e(_tag, '❌ 删除订阅计划失败', e);
rethrow;
}
}
@override
Future<SubscriptionPlan> togglePlanStatus(String id, bool active) async {
try {
AppLogger.d(_tag, '🔄 切换订阅计划状态: id=$id, active=$active');
final response = await _apiClient.patch('/admin/subscription-plans/$id/status', data: {
'active': active,
});
dynamic planData;
if (response is Map<String, dynamic> && response.containsKey('data')) {
planData = response['data'];
} else if (response is Map<String, dynamic>) {
planData = response;
} else {
throw ApiException(-1, '切换订阅计划状态响应格式错误');
}
AppLogger.d(_tag, '✅ 切换订阅计划状态成功: id=$id, active=$active');
return SubscriptionPlan.fromJson(planData as Map<String, dynamic>);
} catch (e) {
AppLogger.e(_tag, '❌ 切换订阅计划状态失败', e);
rethrow;
}
}
@override
Future<SubscriptionStatistics> getSubscriptionStatistics() async {
try {
AppLogger.d(_tag, '📊 获取订阅统计信息');
// TODO: 等后端提供订阅统计接口
// 临时返回模拟数据
await Future.delayed(const Duration(milliseconds: 500));
const statistics = SubscriptionStatistics(
totalPlans: 3,
activePlans: 2,
totalSubscriptions: 150,
activeSubscriptions: 120,
trialSubscriptions: 25,
monthlyRevenue: 5000.0,
yearlyRevenue: 60000.0,
);
AppLogger.d(_tag, '✅ 获取订阅统计信息成功');
return statistics;
} catch (e) {
AppLogger.e(_tag, '❌ 获取订阅统计信息失败', e);
rethrow;
}
}
@override
Future<List<UserSubscription>> getUserSubscriptions(String userId) async {
try {
AppLogger.d(_tag, '🔍 获取用户订阅历史: userId=$userId');
// TODO: 等后端提供用户订阅历史接口
// 临时返回空列表
await Future.delayed(const Duration(milliseconds: 300));
AppLogger.d(_tag, '✅ 获取用户订阅历史成功: userId=$userId');
return [];
} catch (e) {
AppLogger.e(_tag, '❌ 获取用户订阅历史失败', e);
rethrow;
}
}
@override
Future<UserSubscription?> getActiveUserSubscription(String userId) async {
try {
AppLogger.d(_tag, '🔍 获取用户当前订阅: userId=$userId');
// TODO: 等后端提供当前订阅接口
// 临时返回null
await Future.delayed(const Duration(milliseconds: 300));
AppLogger.d(_tag, '✅ 获取用户当前订阅成功: userId=$userId');
return null;
} catch (e) {
AppLogger.e(_tag, '❌ 获取用户当前订阅失败', e);
rethrow;
}
}
}

View File

@@ -0,0 +1,190 @@
import 'package:ainoval/models/ai_request_models.dart';
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/services/api_service/base/api_exception.dart';
import 'package:ainoval/services/api_service/base/sse_client.dart';
import 'package:ainoval/services/api_service/repositories/universal_ai_repository.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:ainoval/utils/date_time_parser.dart';
import 'package:flutter_client_sse/constants/sse_request_type_enum.dart';
/// 通用AI请求仓库实现
class UniversalAIRepositoryImpl implements UniversalAIRepository {
final ApiClient apiClient;
final String _tag = 'UniversalAIRepository';
UniversalAIRepositoryImpl({required this.apiClient});
@override
Future<UniversalAIResponse> sendRequest(UniversalAIRequest request) async {
try {
AppLogger.d(_tag, '发送AI请求: ${request.requestType.value}');
final response = await apiClient.post(
'/ai/universal/process',
data: request.toApiJson(),
);
return UniversalAIResponse.fromJson(response);
} catch (e) {
AppLogger.e(_tag, '发送AI请求失败', e);
rethrow;
}
}
@override
Stream<UniversalAIResponse> streamRequest(UniversalAIRequest request) {
try {
AppLogger.d(_tag, '发送流式AI请求: ${request.requestType.value}');
// 🚀 使用SseClient替代ApiClient复用剧情推演的流式处理逻辑
return SseClient().streamEvents<UniversalAIResponse>(
path: '/ai/universal/stream',
method: SSERequestType.POST,
body: request.toApiJson(),
parser: (json) {
// 🚀 修复:优先检查是否是结束标记
if (json is Map<String, dynamic>) {
final finishReason = json['finishReason'] as String?;
final isComplete = json['isComplete'] as bool? ?? false;
final content = json['content'] as String? ?? '';
// 🚀 如果有结束信号,立即返回结束响应
if (finishReason != null || isComplete || content == '}') {
AppLogger.i(_tag, '检测到流式生成结束信号: finishReason=$finishReason, isComplete=$isComplete, content="$content"');
return UniversalAIResponse(
id: json['id'] as String? ?? 'stream_end_${DateTime.now().millisecondsSinceEpoch}',
requestType: request.requestType,
content: '', // 结束信号内容为空
finishReason: finishReason ?? 'stop',
);
}
}
// 🚀 复用剧情推演的错误处理逻辑
// 首先检查是否是已知的错误格式
if (json is Map<String, dynamic> && json.containsKey('code') && json.containsKey('message')) {
final errorMessage = json['message'] as String? ?? 'Unknown server error';
final errorCodeString = json['code'] as String?;
final errorCode = int.tryParse(errorCodeString ?? '') ?? -1;
AppLogger.e(_tag, '服务器返回已知错误格式: code=${json['code']}, message=$errorMessage');
// 🚀 专门处理积分不足错误
if (errorCodeString == 'INSUFFICIENT_CREDITS') {
throw InsufficientCreditsException(errorMessage);
}
throw ApiException(errorCode, errorMessage);
}
// 检查是否包含 'error' 字段(兼容旧的或不同的错误格式)
else if (json is Map<String, dynamic> && json['error'] != null) {
final errorMessage = json['error'] as String? ?? 'Unknown server error';
AppLogger.e(_tag, '服务器返回错误字段: $errorMessage');
throw ApiException(-1, errorMessage);
}
//AppLogger.v(_tag, '收到流式响应数据: $json');
// 🚀 后端现在返回的是标准的ServerSentEvent<UniversalAIResponseDto>格式
// 直接解析UniversalAIResponseDto
try {
return UniversalAIResponse.fromJson(json);
} catch (e) {
AppLogger.e(_tag, '解析UniversalAIResponse失败: $e, json: $json');
// 🚀 fallback如果解析失败尝试从基本字段构建响应
if (json is Map<String, dynamic>) {
// 处理缺失字段的兼容性
final content = json['content'] as String? ?? '';
final id = json['id'] as String? ?? 'stream_${DateTime.now().millisecondsSinceEpoch}';
final requestType = json['requestType'] as String? ?? request.requestType.value;
final model = json['model'] as String?;
final finishReason = json['finishReason'] as String?;
final createdAtValue = json['createdAt'];
final metadata = json['metadata'] as Map<String, dynamic>? ?? <String, dynamic>{};
// 解析AI请求类型
final aiRequestType = AIRequestType.values.firstWhere(
(type) => type.value == requestType,
orElse: () => request.requestType,
);
// 🚀 使用parseBackendDateTime处理createdAt字段
DateTime? createdAt;
if (createdAtValue != null) {
try {
createdAt = parseBackendDateTime(createdAtValue);
} catch (e) {
AppLogger.w(_tag, '解析createdAt失败使用当前时间: $e');
createdAt = DateTime.now();
}
}
return UniversalAIResponse(
id: id,
requestType: aiRequestType,
content: content,
model: model,
finishReason: finishReason,
createdAt: createdAt,
metadata: metadata,
);
}
// 抛出更具体的解析异常
throw ApiException(-1, '解析响应失败: $e');
}
},
eventName: 'message', // 🚀 与后端保持一致的事件名
connectionId: 'universal_ai_${request.requestType.value}_${DateTime.now().millisecondsSinceEpoch}',
).where((response) {
// 🚀 修复不要过滤掉结束信号即使content为空但有finishReason的响应
if (response.finishReason != null) {
AppLogger.i(_tag, '保留结束信号: finishReason=${response.finishReason}');
return true;
}
// 🚀 只过滤掉既没有内容也没有结束信号的响应
return response.content.isNotEmpty;
});
} catch (e) {
AppLogger.e(_tag, '发送流式AI请求失败', e);
return Stream.error(Exception('流式AI请求失败: ${e.toString()}'));
}
}
@override
Future<UniversalAIPreviewResponse> previewRequest(UniversalAIRequest request) async {
try {
AppLogger.d(_tag, '预览AI请求: ${request.requestType.value}');
final response = await apiClient.post(
'/ai/universal/preview',
data: request.toApiJson(),
);
return UniversalAIPreviewResponse.fromJson(response);
} catch (e) {
AppLogger.e(_tag, '预览AI请求失败', e);
rethrow;
}
}
@override
Future<CostEstimationResponse> estimateCost(UniversalAIRequest request) async {
try {
AppLogger.d(_tag, '预估AI请求积分成本: ${request.requestType.value}');
final response = await apiClient.post(
'/ai/universal/estimate-cost',
data: request.toApiJson(),
);
final costResponse = CostEstimationResponse.fromJson(response);
AppLogger.d(_tag, '积分预估完成 - 预估成本: ${costResponse.estimatedCost}积分, 模型: ${costResponse.modelName}');
return costResponse;
} catch (e) {
AppLogger.e(_tag, '预估AI请求积分成本失败', e);
rethrow;
}
}
}

View File

@@ -0,0 +1,287 @@
import 'dart:async';
import 'package:ainoval/models/user_ai_model_config_model.dart';
import 'package:ainoval/models/model_info.dart';
import 'package:ainoval/services/api_service/base/api_client.dart';
// Api Exception 可能仍然需要,用于类型检查或如果 repository 层需要抛出特定类型的异常
// 但 ApiExceptionHelper 不需要了
import 'package:ainoval/services/api_service/repositories/user_ai_model_config_repository.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:ainoval/blocs/ai_config/ai_config_bloc.dart'; // 添加枚举导入
/// 用户 AI 模型配置仓库实现
class UserAIModelConfigRepositoryImpl implements UserAIModelConfigRepository {
UserAIModelConfigRepositoryImpl({required this.apiClient});
final ApiClient apiClient;
@override
Future<List<String>> listAvailableProviders() async {
AppLogger.i('UserAIModelConfigRepoImpl', '获取可用提供商');
try {
final providers = await apiClient.listAIProviders();
AppLogger.i(
'UserAIModelConfigRepoImpl', '获取可用提供商成功: count=${providers.length}');
return providers;
} catch (e, stackTrace) {
AppLogger.e('UserAIModelConfigRepoImpl', '获取可用提供商失败', e, stackTrace);
// 直接重新抛出ApiClient 会处理 DioException 转换
rethrow;
}
}
@override
Future<List<ModelInfo>> listModelsForProvider(String provider) async {
AppLogger.i('UserAIModelConfigRepoImpl', '获取提供商 $provider 模型信息');
try {
final models = await apiClient.listAIModelsForProvider(provider: provider);
AppLogger.i('UserAIModelConfigRepoImpl',
'获取提供商 $provider 模型信息成功: count=${models.length}');
return models;
} catch (e, stackTrace) {
AppLogger.e(
'UserAIModelConfigRepoImpl', '获取提供商 $provider 模型信息失败', e, stackTrace);
rethrow;
}
}
@override
Future<UserAIModelConfigModel> addConfiguration({
required String userId,
required String provider,
required String modelName,
String? alias,
required String apiKey,
String? apiEndpoint,
}) async {
AppLogger.i('UserAIModelConfigRepoImpl',
'添加配置: userId=$userId'); // Mask apiKey in logs
try {
final config = await apiClient.addAIConfiguration(
userId: userId,
provider: provider,
modelName: modelName,
alias: alias,
apiKey: apiKey,
apiEndpoint: apiEndpoint,
);
AppLogger.i('UserAIModelConfigRepoImpl',
'添加配置成功: userId=$userId, configId=${config.id}');
return config;
} catch (e, stackTrace) {
AppLogger.e(
'UserAIModelConfigRepoImpl', '添加配置失败: userId=$userId', e, stackTrace);
// 直接重新抛出
rethrow;
}
}
@override
Future<List<UserAIModelConfigModel>> listConfigurations({
required String userId,
bool? validatedOnly,
}) async {
AppLogger.i('UserAIModelConfigRepoImpl',
'列出配置(包含API密钥): userId=$userId, validatedOnly=$validatedOnly');
try {
// 调用新的API端点获取包含解密后API密钥的配置列表
final configs = await apiClient.listAIConfigurationsWithDecryptedKeys(
userId: userId,
validatedOnly: validatedOnly,
);
AppLogger.i('UserAIModelConfigRepoImpl',
'列出配置(包含API密钥)成功: userId=$userId, count=${configs.length}');
return configs;
} catch (e, stackTrace) {
AppLogger.e(
'UserAIModelConfigRepoImpl', '列出配置(包含API密钥)失败: userId=$userId', e, stackTrace);
// 直接重新抛出
rethrow;
}
}
@override
Future<UserAIModelConfigModel> getConfigurationById({
required String userId,
required String configId,
}) async {
AppLogger.i('UserAIModelConfigRepoImpl',
'获取配置: userId=$userId, configId=$configId');
try {
final config = await apiClient.getAIConfigurationById(
userId: userId,
configId: configId,
);
AppLogger.i('UserAIModelConfigRepoImpl',
'获取配置成功: userId=$userId, configId=${config.id}');
return config;
} catch (e, stackTrace) {
AppLogger.e('UserAIModelConfigRepoImpl',
'获取配置失败: userId=$userId, configId=$configId', e, stackTrace);
// 直接重新抛出
rethrow;
}
}
@override
Future<UserAIModelConfigModel> updateConfiguration({
required String userId,
required String configId,
String? alias,
String? apiKey,
String? apiEndpoint,
}) async {
if (alias == null && apiKey == null && apiEndpoint == null) {
AppLogger.w('UserAIModelConfigRepoImpl',
'更新配置调用,但没有提供要更新的字段: userId=$userId, configId=$configId');
AppLogger.i('UserAIModelConfigRepoImpl', '无有效更新字段,尝试获取当前配置');
// 注意:这里的 getConfigurationById 本身也可能抛出异常
return getConfigurationById(userId: userId, configId: configId);
}
AppLogger.i('UserAIModelConfigRepoImpl',
'更新配置: userId=$userId, configId=$configId'); // Mask apiKey
try {
final config = await apiClient.updateAIConfiguration(
userId: userId,
configId: configId,
alias: alias,
apiKey: apiKey,
apiEndpoint: apiEndpoint,
);
AppLogger.i('UserAIModelConfigRepoImpl',
'更新配置成功: userId=$userId, configId=${config.id}');
return config;
} catch (e, stackTrace) {
AppLogger.e('UserAIModelConfigRepoImpl',
'更新配置失败: userId=$userId, configId=$configId', e, stackTrace);
// 直接重新抛出
rethrow;
}
}
@override
Future<void> deleteConfiguration({
required String userId,
required String configId,
}) async {
AppLogger.i('UserAIModelConfigRepoImpl',
'删除配置: userId=$userId, configId=$configId');
try {
await apiClient.deleteAIConfiguration(userId: userId, configId: configId);
AppLogger.i('UserAIModelConfigRepoImpl',
'删除配置成功: userId=$userId, configId=$configId');
} catch (e, stackTrace) {
AppLogger.e('UserAIModelConfigRepoImpl',
'删除配置失败: userId=$userId, configId=$configId', e, stackTrace);
// 直接重新抛出
rethrow;
}
}
@override
Future<UserAIModelConfigModel> validateConfiguration({
required String userId,
required String configId,
}) async {
AppLogger.i('UserAIModelConfigRepoImpl',
'验证配置: userId=$userId, configId=$configId');
try {
final config = await apiClient.validateAIConfiguration(
userId: userId,
configId: configId,
);
AppLogger.i('UserAIModelConfigRepoImpl',
'验证配置成功: userId=$userId, configId=${config.id}, isValidated=${config.isValidated}');
return config;
} catch (e, stackTrace) {
AppLogger.e('UserAIModelConfigRepoImpl',
'验证配置失败: userId=$userId, configId=$configId', e, stackTrace);
// 直接重新抛出
rethrow;
}
}
@override
Future<UserAIModelConfigModel> setDefaultConfiguration({
required String userId,
required String configId,
}) async {
AppLogger.i('UserAIModelConfigRepoImpl',
'设置默认配置: userId=$userId, configId=$configId');
try {
final config = await apiClient.setDefaultAIConfiguration(
userId: userId,
configId: configId,
);
AppLogger.i('UserAIModelConfigRepoImpl',
'设置默认配置成功: userId=$userId, configId=${config.id}, isDefault=${config.isDefault}');
return config;
} catch (e, stackTrace) {
AppLogger.e('UserAIModelConfigRepoImpl',
'设置默认配置失败: userId=$userId, configId=$configId', e, stackTrace);
// 直接重新抛出
rethrow;
}
}
@override
Future<ModelListingCapability> getProviderCapability(String providerName) async {
AppLogger.i('UserAIModelConfigRepoImpl', '获取提供商 $providerName 的模型列表能力');
try {
final capabilityString = await apiClient.getProviderCapability(providerName);
AppLogger.i('UserAIModelConfigRepoImpl', '获取提供商 $providerName 的模型列表能力成功: $capabilityString');
// 清理字符串,去除可能的前后引号
var cleanCapabilityString = capabilityString;
if (cleanCapabilityString.startsWith('"') && cleanCapabilityString.endsWith('"')) {
cleanCapabilityString = cleanCapabilityString.substring(1, cleanCapabilityString.length - 1);
}
ModelListingCapability capability;
// 使用清理后的字符串进行比较
switch (cleanCapabilityString) {
case 'NO_LISTING':
capability = ModelListingCapability.noListing;
break;
case 'LISTING_WITHOUT_KEY':
capability = ModelListingCapability.listingWithoutKey;
break;
case 'LISTING_WITH_KEY':
capability = ModelListingCapability.listingWithKey;
break;
default:
AppLogger.w('UserAIModelConfigRepoImpl', '未知的提供商能力字符串: $capabilityString, 使用默认 noListing');
capability = ModelListingCapability.noListing;
}
AppLogger.i('UserAIModelConfigRepoImpl', '获取提供商 $providerName 的模型列表能力成功: $capability');
return capability;
} catch (e, stackTrace) {
AppLogger.e('UserAIModelConfigRepoImpl', '获取提供商 $providerName 的模型列表能力失败', e, stackTrace);
// 如果出错,默认为最安全的能力类型
AppLogger.w('UserAIModelConfigRepoImpl', '使用默认能力类型 noListing');
return ModelListingCapability.noListing;
}
}
@override
Future<List<ModelInfo>> listModelsWithApiKey({
required String provider,
required String apiKey,
String? apiEndpoint,
}) async {
AppLogger.i('UserAIModelConfigRepoImpl', '使用API密钥获取提供商 $provider 的模型信息列表');
try {
final models = await apiClient.listAIModelsWithApiKey(
provider: provider,
apiKey: apiKey,
apiEndpoint: apiEndpoint,
);
AppLogger.i('UserAIModelConfigRepoImpl',
'使用API密钥获取提供商 $provider 的模型信息列表成功: count=${models.length}');
return models;
} catch (e, stackTrace) {
AppLogger.e('UserAIModelConfigRepoImpl', '使用API密钥获取提供商 $provider 的模型信息列表失败', e, stackTrace);
rethrow;
}
}
}

View File

@@ -0,0 +1,83 @@
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/utils/logger.dart';
class UserAnalyticsRepositoryImpl {
final ApiClient _apiClient;
final String _tag = 'UserAnalyticsRepository';
UserAnalyticsRepositoryImpl({ApiClient? apiClient}) : _apiClient = apiClient ?? ApiClient();
Future<Map<String, int>> getMyDailyWords({DateTime? start, DateTime? end}) async {
try {
final qp = <String, dynamic>{};
if (start != null) qp['start'] = start.toIso8601String();
if (end != null) qp['end'] = end.toIso8601String();
final res = await _apiClient.getWithParams('/analytics/writing/daily', queryParameters: qp);
if (res is Map<String, dynamic> && res['data'] is Map<String, dynamic>) {
final data = res['data'] as Map<String, dynamic>;
final daily = (data['dailyWords'] as Map).map((k, v) => MapEntry(k.toString(), int.tryParse(v.toString()) ?? 0));
return daily;
}
return {};
} catch (e) {
AppLogger.e(_tag, '获取每日写作字数失败', e);
return {};
}
}
Future<Map<String, dynamic>> getMyWordsBySource({DateTime? start, DateTime? end}) async {
try {
final qp = <String, dynamic>{};
if (start != null) qp['start'] = start.toIso8601String();
if (end != null) qp['end'] = end.toIso8601String();
final res = await _apiClient.getWithParams('/analytics/writing/source', queryParameters: qp);
if (res is Map<String, dynamic> && res['data'] is Map<String, dynamic>) {
return res['data'] as Map<String, dynamic>;
}
return {};
} catch (e) {
AppLogger.e(_tag, '获取写作来源统计失败', e);
return {};
}
}
Future<Map<String, int>> getMyDailyTokens({DateTime? start, DateTime? end}) async {
try {
final qp = <String, dynamic>{};
if (start != null) qp['startTime'] = start.toIso8601String();
if (end != null) qp['endTime'] = end.toIso8601String();
final res = await _apiClient.getWithParams('/analytics/llm/daily-tokens', queryParameters: qp);
if (res is Map<String, dynamic> && res['data'] is Map<String, dynamic>) {
final map = <String, int>{};
(res['data'] as Map<String, dynamic>).forEach((k, v) {
map[k] = int.tryParse(v.toString()) ?? 0;
});
return map;
}
return {};
} catch (e) {
AppLogger.e(_tag, '获取每日Token失败', e);
return {};
}
}
Future<Map<String, dynamic>> getMyFeatureUsage({DateTime? start, DateTime? end}) async {
try {
final qp = <String, dynamic>{};
if (start != null) qp['startTime'] = start.toIso8601String();
if (end != null) qp['endTime'] = end.toIso8601String();
final res = await _apiClient.getWithParams('/analytics/llm/features', queryParameters: qp);
if (res is Map<String, dynamic> && res['data'] is Map<String, dynamic>) {
return res['data'] as Map<String, dynamic>;
}
return {};
} catch (e) {
AppLogger.e(_tag, '获取功能使用统计失败', e);
return {};
}
}
}

View File

@@ -0,0 +1,32 @@
import 'package:ainoval/models/next_outline/next_outline_dto.dart';
import 'package:ainoval/models/next_outline/outline_generation_chunk.dart';
/// 剧情推演仓库接口
abstract class NextOutlineRepository {
/// 流式生成剧情大纲
///
/// [novelId] 小说ID
/// [request] 生成请求
Stream<OutlineGenerationChunk> generateNextOutlinesStream(
String novelId,
GenerateNextOutlinesRequest request
);
/// 重新生成单个剧情大纲选项
///
/// [novelId] 小说ID
/// [request] 重新生成请求
Stream<OutlineGenerationChunk> regenerateOutlineOption(
String novelId,
RegenerateOptionRequest request
);
/// 保存选中的剧情大纲
///
/// [novelId] 小说ID
/// [request] 保存请求
Future<SaveNextOutlineResponse> saveNextOutline(
String novelId,
SaveNextOutlineRequest request
);
}

View File

@@ -0,0 +1,12 @@
import 'package:ainoval/models/novel_setting_item.dart';
abstract class NovelAIRepository {
Future<List<NovelSettingItem>> generateNovelSettings({
required String novelId,
required String startChapterId,
String? endChapterId,
required List<String> settingTypes,
required int maxSettingsPerType,
required String additionalInstructions,
});
}

View File

@@ -0,0 +1,154 @@
import 'package:ainoval/models/import_status.dart';
import 'package:ainoval/models/novel_structure.dart';
import 'package:ainoval/models/scene_version.dart';
import 'package:ainoval/models/chapters_for_preload_dto.dart';
/// 小说仓库接口
///
/// 定义与小说相关的所有API操作
abstract class NovelRepository {
/// 获取所有小说
Future<List<Novel>> fetchNovels();
/// 获取单个小说
Future<Novel> fetchNovel(String id);
/// 获取单个小说场景内容纯文本格式
Future<Novel> fetchNovelText(String id);
/// 获取单个小说
Future<Novel> fetchNovelOnlyStructure(String id);
/// 创建小说
Future<Novel> createNovel(String title,
{String? description, String? coverImage});
/// 根据作者ID获取小说列表
Future<List<Novel>> fetchNovelsByAuthor(String authorId);
/// 搜索小说
Future<List<Novel>> searchNovelsByTitle(String title);
/// 删除小说
Future<void> deleteNovel(String id);
/// 获取场景内容
Future<Scene> fetchSceneContent(
String novelId, String actId, String chapterId, String sceneId);
/// 更新场景内容
Future<Scene> updateSceneContent(String novelId, String actId,
String chapterId, String sceneId, Scene scene);
/// 更新摘要内容
Future<Summary> updateSummary(String novelId, String actId, String chapterId,
String sceneId, Summary summary);
/// 更新场景内容并保存历史版本
Future<Scene> updateSceneContentWithHistory(String novelId, String chapterId,
String sceneId, String content, String userId, String reason);
/// 获取场景的历史版本列表
Future<List<SceneHistoryEntry>> getSceneHistory(
String novelId, String chapterId, String sceneId);
/// 恢复场景到指定的历史版本
Future<Scene> restoreSceneVersion(String novelId, String chapterId,
String sceneId, int historyIndex, String userId, String reason);
/// 对比两个场景版本
Future<SceneVersionDiff> compareSceneVersions(String novelId,
String chapterId, String sceneId, int versionIndex1, int versionIndex2);
/// 导入小说文件(传统方式,向后兼容)
///
/// 返回导入任务的ID
Future<String> importNovel(List<int> fileBytes, String fileName);
// === 新的三步导入流程方法 ===
/// 第一步上传文件获取预览会话ID
///
/// - [fileBytes]: 文件字节数据
/// - [fileName]: 文件名
/// - 返回: 预览会话ID
Future<String> uploadFileForPreview(List<int> fileBytes, String fileName);
/// 第二步:获取导入预览
///
/// - [fileSessionId]: 预览会话ID
/// - [customTitle]: 自定义标题
/// - [chapterLimit]: 章节数量限制
/// - [enableSmartContext]: 是否启用智能上下文
/// - [enableAISummary]: 是否启用AI摘要
/// - [aiConfigId]: AI配置ID
/// - [previewChapterCount]: 预览章节数量
/// - 返回: 导入预览响应数据
Future<Map<String, dynamic>> getImportPreview({
required String fileSessionId,
String? customTitle,
int? chapterLimit,
bool enableSmartContext = true,
bool enableAISummary = false,
String? aiConfigId,
int previewChapterCount = 10,
});
/// 第三步:确认并开始导入
///
/// - [previewSessionId]: 预览会话ID
/// - [finalTitle]: 最终确认的标题
/// - [selectedChapterIndexes]: 选中的章节索引列表
/// - [enableSmartContext]: 是否启用智能上下文
/// - [enableAISummary]: 是否启用AI摘要
/// - [aiConfigId]: AI配置ID
/// - 返回: 导入任务ID
Future<String> confirmAndStartImport({
required String previewSessionId,
required String finalTitle,
List<int>? selectedChapterIndexes,
bool enableSmartContext = true,
bool enableAISummary = false,
String? aiConfigId,
});
/// 清理预览会话
///
/// - [previewSessionId]: 预览会话ID
Future<void> cleanupPreviewSession(String previewSessionId);
/// 获取导入任务状态流
///
/// 返回导入状态的实时更新
Stream<ImportStatus> getImportStatus(String jobId);
/// 取消导入任务
///
/// - [jobId]: 导入任务ID
/// - 返回: 是否成功取消
Future<bool> cancelImport(String jobId);
/// 获取当前章节后面指定数量的章节和场景内容
///
/// 允许跨卷加载,专门用于阅读器的分批加载功能
/// - [novelId]: 小说ID
/// - [currentChapterId]: 当前章节ID
/// - [chaptersLimit]: 要加载的章节数量默认为3
/// - 返回: 包含小说信息和后续章节场景数据的Novel对象
Future<Novel?> fetchChaptersAfter(String novelId, String currentChapterId, {int chaptersLimit = 3, bool includeCurrentChapter = true});
/// 获取指定章节后面的章节列表(用于预加载)
///
/// 专门为预加载功能设计,只返回章节列表和场景内容,不返回完整小说结构
/// - [novelId]: 小说ID
/// - [currentChapterId]: 当前章节ID
/// - [chaptersLimit]: 要获取的章节数量限制默认为3
/// - [includeCurrentChapter]: 是否包含当前章节默认为false
/// - 返回: 包含章节列表和场景数据的ChaptersForPreloadDto
Future<ChaptersForPreloadDto?> fetchChaptersForPreload(
String novelId,
String currentChapterId, {
int chaptersLimit = 3,
bool includeCurrentChapter = false,
});
}

View File

@@ -0,0 +1,147 @@
import 'package:ainoval/models/novel_setting_item.dart';
import 'package:ainoval/models/setting_group.dart';
/// 小说设定仓储接口
abstract class NovelSettingRepository {
// ==================== 设定条目管理 ====================
/// 创建小说设定条目
Future<NovelSettingItem> createSettingItem({
required String novelId,
required NovelSettingItem settingItem,
});
/// 获取小说设定条目列表
Future<List<NovelSettingItem>> getNovelSettingItems({
required String novelId,
String? type,
String? name,
int? priority,
String? generatedBy,
String? status,
required int page,
required int size,
required String sortBy,
required String sortDirection,
});
/// 获取小说设定条目详情
Future<NovelSettingItem> getSettingItemDetail({
required String novelId,
required String itemId,
});
/// 更新小说设定条目
Future<NovelSettingItem> updateSettingItem({
required String novelId,
required String itemId,
required NovelSettingItem settingItem,
});
/// 删除小说设定条目
Future<void> deleteSettingItem({
required String novelId,
required String itemId,
});
/// 添加设定条目之间的关系
Future<NovelSettingItem> addSettingRelationship({
required String novelId,
required String itemId,
required String targetItemId,
required String relationshipType,
String? description,
});
/// 删除设定条目之间的关系
Future<void> removeSettingRelationship({
required String novelId,
required String itemId,
required String targetItemId,
required String relationshipType,
});
/// 设置父子关系
Future<NovelSettingItem> setParentChildRelationship({
required String novelId,
required String childId,
required String parentId,
});
/// 移除父子关系
Future<NovelSettingItem> removeParentChildRelationship({
required String novelId,
required String childId,
});
// ==================== 设定组管理 ====================
/// 创建设定组
Future<SettingGroup> createSettingGroup({
required String novelId,
required SettingGroup settingGroup,
});
/// 获取小说的设定组列表
Future<List<SettingGroup>> getNovelSettingGroups({
required String novelId,
String? name,
bool? isActiveContext,
});
/// 获取设定组详情
Future<SettingGroup> getSettingGroupDetail({
required String novelId,
required String groupId,
});
/// 更新设定组
Future<SettingGroup> updateSettingGroup({
required String novelId,
required String groupId,
required SettingGroup settingGroup,
});
/// 删除设定组
Future<void> deleteSettingGroup({
required String novelId,
required String groupId,
});
/// 添加设定条目到设定组
Future<SettingGroup> addItemToGroup({
required String novelId,
required String groupId,
required String itemId,
});
/// 从设定组中移除设定条目
Future<void> removeItemFromGroup({
required String novelId,
required String groupId,
required String itemId,
});
/// 激活/停用设定组作为上下文
Future<SettingGroup> setGroupActiveContext({
required String novelId,
required String groupId,
required bool isActive,
});
// ==================== 高级功能 ====================
/// 从文本中自动提取设定条目
Future<List<NovelSettingItem>> extractSettingsFromText({
required String novelId,
required String text,
required String type,
});
/// 根据关键词搜索设定条目
Future<List<NovelSettingItem>> searchSettingItems({
required String novelId,
required String query,
List<String>? types,
List<String>? groupIds,
double? minScore,
int? maxResults,
});
}

View File

@@ -0,0 +1,103 @@
import 'package:ainoval/models/novel_snippet.dart';
/// 小说片段仓储接口
///
/// 定义与小说片段相关的所有API操作
abstract class NovelSnippetRepository {
/// 创建片段
///
/// [request] 创建片段请求数据
/// 返回创建的片段信息
Future<NovelSnippet> createSnippet(CreateSnippetRequest request);
/// 获取小说的所有片段(分页)
///
/// [novelId] 小说ID
/// [page] 页码默认为0
/// [size] 每页大小默认为20
/// 返回分页片段数据
Future<SnippetPageResult<NovelSnippet>> getSnippetsByNovelId(
String novelId, {
int page = 0,
int size = 20,
});
/// 获取片段详情
///
/// [snippetId] 片段ID
/// 返回片段详细信息(会增加浏览次数)
Future<NovelSnippet> getSnippetDetail(String snippetId);
/// 更新片段内容
///
/// [request] 更新内容请求数据
/// 返回更新后的片段信息
Future<NovelSnippet> updateSnippetContent(UpdateSnippetContentRequest request);
/// 更新片段标题
///
/// [request] 更新标题请求数据
/// 返回更新后的片段信息
Future<NovelSnippet> updateSnippetTitle(UpdateSnippetTitleRequest request);
/// 收藏/取消收藏片段
///
/// [request] 更新收藏状态请求数据
/// 返回更新后的片段信息
Future<NovelSnippet> updateSnippetFavorite(UpdateSnippetFavoriteRequest request);
/// 获取片段历史记录
///
/// [snippetId] 片段ID
/// [page] 页码默认为0
/// [size] 每页大小默认为10
/// 返回分页历史记录数据
Future<SnippetPageResult<NovelSnippetHistory>> getSnippetHistory(
String snippetId, {
int page = 0,
int size = 10,
});
/// 预览历史版本内容
///
/// [snippetId] 片段ID
/// [version] 版本号
/// 返回指定版本的历史记录
Future<NovelSnippetHistory> previewHistoryVersion(String snippetId, int version);
/// 回退到历史版本(创建新片段)
///
/// [request] 回退版本请求数据
/// 返回新创建的片段信息
Future<NovelSnippet> revertToHistoryVersion(RevertSnippetVersionRequest request);
/// 删除片段
///
/// [snippetId] 片段ID
/// 执行软删除操作
Future<void> deleteSnippet(String snippetId);
/// 获取用户收藏的片段
///
/// [page] 页码默认为0
/// [size] 每页大小默认为20
/// 返回分页收藏片段数据
Future<SnippetPageResult<NovelSnippet>> getFavoriteSnippets({
int page = 0,
int size = 20,
});
/// 搜索片段
///
/// [novelId] 小说ID
/// [searchText] 搜索文本
/// [page] 页码默认为0
/// [size] 每页大小默认为20
/// 返回搜索结果分页数据
Future<SnippetPageResult<NovelSnippet>> searchSnippets(
String novelId,
String searchText, {
int page = 0,
int size = 20,
});
}

View File

@@ -0,0 +1,81 @@
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/utils/logger.dart';
enum PayChannel { wechat, alipay }
class PaymentOrderDto {
final String id;
final String outTradeNo;
final String planId;
final String paymentUrl;
final String status;
PaymentOrderDto({
required this.id,
required this.outTradeNo,
required this.planId,
required this.paymentUrl,
required this.status,
});
factory PaymentOrderDto.fromJson(Map<String, dynamic> json) => PaymentOrderDto(
id: json['id'] ?? '',
outTradeNo: json['outTradeNo'] ?? '',
planId: json['planId'] ?? '',
paymentUrl: json['paymentUrl'] ?? '',
status: json['status']?.toString() ?? '',
);
}
class PaymentRepository {
final ApiClient _apiClient;
final String _tag = 'PaymentRepository';
PaymentRepository({ApiClient? apiClient}) : _apiClient = apiClient ?? ApiClient();
Future<PaymentOrderDto> createPayment({
required String planId,
required PayChannel channel,
}) async {
try {
final res = await _apiClient.post('/payments/create/$planId?channel=${channel.name.toUpperCase()}');
if (res is Map<String, dynamic> && res['data'] is Map<String, dynamic>) {
return PaymentOrderDto.fromJson(res['data'] as Map<String, dynamic>);
}
throw Exception('创建支付订单失败');
} catch (e) {
AppLogger.e(_tag, '创建支付订单失败', e);
rethrow;
}
}
Future<PaymentOrderDto> createCreditPackPayment({
required String planId,
required PayChannel channel,
}) async {
try {
final res = await _apiClient.post('/payments/create-credit-pack/$planId?channel=${channel.name.toUpperCase()}');
if (res is Map<String, dynamic> && res['data'] is Map<String, dynamic>) {
return PaymentOrderDto.fromJson(res['data'] as Map<String, dynamic>);
}
throw Exception('创建积分包支付订单失败');
} catch (e) {
AppLogger.e(_tag, '创建积分包支付订单失败', e);
rethrow;
}
}
Future<List<PaymentOrderDto>> myOrders() async {
try {
final res = await _apiClient.get('/payments/my-orders');
if (res is List) {
return res.map((e) => PaymentOrderDto.fromJson(e as Map<String, dynamic>)).toList();
}
return [];
} catch (e) {
AppLogger.e(_tag, '获取我的订单失败', e);
return [];
}
}
}

View File

@@ -0,0 +1,53 @@
import 'package:ainoval/models/preset_models.dart';
/// 预设聚合仓储接口
/// 提供一站式的预设获取和缓存接口
abstract class PresetAggregationRepository {
/// 获取功能的完整预设包
/// [featureType] 功能类型
/// [novelId] 小说ID可选
/// 返回完整预设包,包含系统预设、用户预设、快捷访问预设等全部信息
Future<PresetPackage> getCompletePresetPackage(
String featureType, {
String? novelId,
});
/// 获取用户的预设概览
/// 返回跨功能统计信息用于用户Dashboard
Future<UserPresetOverview> getUserPresetOverview();
/// 批量获取多个功能的预设包
/// [featureTypes] 功能类型列表如果为null则获取所有类型
/// [novelId] 小说ID可选
/// 返回功能类型到预设包的映射,用于前端初始化时一次性获取所有需要的数据
Future<Map<String, PresetPackage>> getBatchPresetPackages({
List<String>? featureTypes,
String? novelId,
});
/// 预热用户缓存
/// 系统启动或用户登录时调用,提升后续响应速度
/// 返回缓存预热结果
Future<CacheWarmupResult> warmupCache();
/// 获取系统缓存统计
/// 用于系统监控和性能分析
/// 返回聚合服务的缓存统计信息
Future<AggregationCacheStats> getCacheStats();
/// 清除预设聚合缓存
/// 用于调试和强制刷新缓存
/// 返回清除结果消息
Future<String> clearCache();
/// 聚合服务健康检查
/// 检查预设聚合服务的健康状态
/// 返回健康状态信息
Future<Map<String, dynamic>> healthCheck();
/// 🚀 获取用户的所有预设聚合数据
/// 一次性返回用户的所有预设相关数据避免多次API调用
/// [novelId] 小说ID可选
/// 返回完整的用户预设聚合数据
Future<AllUserPresetData> getAllUserPresetData({String? novelId});
}

View File

@@ -0,0 +1,194 @@
import 'package:ainoval/models/prompt_models.dart';
/// 提示词管理接口
abstract class PromptRepository {
/// 获取所有提示词
Future<Map<AIFeatureType, PromptData>> getAllPrompts();
/// 获取指定功能类型的提示词
Future<PromptData> getPrompt(AIFeatureType featureType);
/// 保存提示词
Future<PromptData> savePrompt(AIFeatureType featureType, String promptText);
/// 删除提示词(恢复为默认)
Future<PromptData> deletePrompt(AIFeatureType featureType);
/// 获取提示词模板列表
Future<List<PromptTemplate>> getPromptTemplates();
/// 获取指定功能类型的提示词模板列表
Future<List<PromptTemplate>> getPromptTemplatesByFeatureType(AIFeatureType featureType);
/// 获取提示词模板详情
Future<PromptTemplate> getPromptTemplateById(String templateId);
/// 从公共模板复制创建私有模板
Future<PromptTemplate> copyPublicTemplate(PromptTemplate template);
/// 切换模板收藏状态
Future<PromptTemplate> toggleTemplateFavorite(PromptTemplate template);
/// 创建提示词模板
Future<PromptTemplate> createPromptTemplate({
required String name,
required String content,
required AIFeatureType featureType,
required String authorId,
String? description,
List<String>? tags,
});
/// 更新提示词模板
Future<PromptTemplate> updatePromptTemplate({
required String templateId,
String? name,
String? content,
});
/// 删除提示词模板
Future<void> deletePromptTemplate(String templateId);
/// 流式优化提示词
void optimizePromptStream(
String templateId,
OptimizePromptRequest request, {
Function(double)? onProgress,
Function(OptimizationResult)? onResult,
Function(String)? onError,
});
/// 取消优化
void cancelOptimization();
/// 优化提示词
Future<OptimizationResult> optimizePrompt({
required String templateId,
required OptimizePromptRequest request,
});
/// 生成场景摘要
Future<String> generateSceneSummary({
required String novelId,
required String sceneId,
});
/// 从摘要生成场景
Future<String> generateSceneFromSummary({
required String novelId,
required String summary,
});
// ====================== 统一提示词聚合接口 ======================
/// 获取功能的完整提示词包
/// 包含系统默认、用户自定义、公开模板、最近使用等全部信息
Future<PromptPackage> getCompletePromptPackage(
AIFeatureType featureType, {
bool includePublic = true,
});
/// 获取用户的提示词概览
/// 跨功能统计信息用于用户Dashboard
Future<UserPromptOverview> getUserPromptOverview();
/// 批量获取多个功能的提示词包
/// 用于前端初始化时一次性获取所有需要的数据
Future<Map<AIFeatureType, PromptPackage>> getBatchPromptPackages({
List<AIFeatureType>? featureTypes,
bool includePublic = true,
});
/// 预热用户缓存
/// 系统启动或用户登录时调用,提升后续响应速度
Future<CacheWarmupResult> warmupCache();
/// 获取系统缓存统计
/// 用于系统监控和性能分析
Future<AggregationCacheStats> getCacheStats();
/// 获取虚拟线程性能统计
/// 用于监控占位符解析性能
Future<PlaceholderPerformanceStats> getPlaceholderPerformanceStats();
/// 健康检查接口
/// 检查聚合服务是否正常工作
Future<SystemHealthStatus> healthCheck();
// ====================== 增强用户提示词模板管理接口 ======================
/// 创建增强用户提示词模板
Future<EnhancedUserPromptTemplate> createEnhancedPromptTemplate(
CreatePromptTemplateRequest request,
);
/// 更新增强用户提示词模板
Future<EnhancedUserPromptTemplate> updateEnhancedPromptTemplate(
String templateId,
UpdatePromptTemplateRequest request,
);
/// 删除增强用户提示词模板
Future<void> deleteEnhancedPromptTemplate(String templateId);
/// 获取增强用户提示词模板详情
Future<EnhancedUserPromptTemplate?> getEnhancedPromptTemplate(String templateId);
/// 获取用户所有增强提示词模板
Future<List<EnhancedUserPromptTemplate>> getUserEnhancedPromptTemplates({
AIFeatureType? featureType,
});
/// 获取用户收藏的增强模板
Future<List<EnhancedUserPromptTemplate>> getUserFavoriteEnhancedTemplates();
/// 获取最近使用的增强模板
Future<List<EnhancedUserPromptTemplate>> getRecentlyUsedEnhancedTemplates({
int limit = 10,
});
/// 发布模板为公开
Future<EnhancedUserPromptTemplate> publishEnhancedTemplate(
String templateId,
PublishTemplateRequest request,
);
/// 通过分享码获取模板
Future<EnhancedUserPromptTemplate?> getEnhancedTemplateByShareCode(String shareCode);
/// 复制公开增强模板
Future<EnhancedUserPromptTemplate> copyPublicEnhancedTemplate(String templateId);
/// 获取公开增强模板列表
Future<List<EnhancedUserPromptTemplate>> getPublicEnhancedTemplates(
AIFeatureType featureType, {
int page = 0,
int size = 20,
});
/// 收藏增强模板
Future<void> favoriteEnhancedTemplate(String templateId);
/// 取消收藏增强模板
Future<void> unfavoriteEnhancedTemplate(String templateId);
/// 评分增强模板
Future<EnhancedUserPromptTemplate> rateEnhancedTemplate(
String templateId,
int rating,
);
/// 记录增强模板使用
Future<void> recordEnhancedTemplateUsage(String templateId);
/// 获取用户所有标签
Future<List<String>> getUserPromptTags();
// ==================== 默认模板功能 ====================
/// 设置默认模板
Future<EnhancedUserPromptTemplate> setDefaultEnhancedTemplate(String templateId);
/// 获取默认模板
Future<EnhancedUserPromptTemplate?> getDefaultEnhancedTemplate(AIFeatureType featureType);
}

View File

@@ -0,0 +1,9 @@
import '../../../models/public_model_config.dart';
/// 公共模型仓库接口
abstract interface class PublicModelRepository {
/// 获取公共模型列表
/// 只包含向前端暴露的安全信息不含API Keys等敏感数据
/// 用户必须登录才能访问此接口
Future<List<PublicModel>> getPublicModels();
}

View File

@@ -0,0 +1,252 @@
import '../../../models/setting_generation_session.dart';
import '../../../models/setting_generation_event.dart';
import '../../../models/strategy_template_info.dart';
import '../../../models/save_result.dart';
import '../../../models/ai_request_models.dart';
/// 设定生成仓库接口
///
/// 核心功能说明:
/// 1. 设定生成流程管理支持AI生成和修改设定节点
/// 2. 用户维度历史记录管理:不再依赖特定小说,支持跨小说使用
/// 3. 编辑会话管理:支持从小说设定或历史记录创建编辑会话
/// 4. 历史记录操作:复制、删除、恢复等完整的历史记录管理功能
abstract class SettingGenerationRepository {
/// 获取可用的生成策略模板
Future<List<StrategyTemplateInfo>> getAvailableStrategies();
/// 启动设定生成
Stream<SettingGenerationEvent> startGeneration({
required String initialPrompt,
required String promptTemplateId,
String? novelId,
required String modelConfigId,
String? userId,
bool? usePublicTextModel,
String? textPhasePublicProvider,
String? textPhasePublicModelId,
});
/// 从小说设定创建编辑会话
///
/// 支持用户选择编辑模式:
/// - createNewSnapshot = true创建新的设定快照
/// - createNewSnapshot = false编辑上次的设定
Future<Map<String, dynamic>> startSessionFromNovel({
required String novelId,
required String editReason,
required String modelConfigId,
required bool createNewSnapshot,
});
/// 强制关闭所有与设定生成相关的SSE连接用于彻底停止自动重连
Future<void> forceCloseAllSSE();
/// 修改设定节点
Stream<SettingGenerationEvent> updateNode({
required String sessionId,
required String nodeId,
required String modificationPrompt,
required String modelConfigId,
String scope = 'self',
});
/// 基于会话整体调整生成
Stream<SettingGenerationEvent> adjustSession({
required String sessionId,
required String adjustmentPrompt,
required String modelConfigId,
String? promptTemplateId,
});
/// 直接更新节点内容
Future<String> updateNodeContent({
required String sessionId,
required String nodeId,
required String newContent,
});
/// 保存生成的设定
///
/// [novelId] 为 null 时表示保存为独立快照(不关联任何小说)
/// 返回包含根设定ID列表和历史记录ID的完整结果
Future<SaveResult> saveGeneratedSettings({
required String sessionId,
String? novelId,
bool updateExisting = false,
String? targetHistoryId,
});
/// 获取会话状态
Future<Map<String, dynamic>> getSessionStatus({
required String sessionId,
});
/// 加载历史记录详情(包含完整节点数据)
Future<Map<String, dynamic>> loadHistoryDetail({
required String historyId,
});
/// 取消生成会话
Future<void> cancelSession({
required String sessionId,
});
// ==================== NOVEL_COMPOSE 流式写作编排 ====================
/// 基于设定/提示词的写作编排(大纲/章节/组合)流式生成
/// 统一走通用AI通道/ai/universal/stream传入 AIRequestType.NOVEL_COMPOSE
Stream<UniversalAIResponse> composeStream({
required UniversalAIRequest request,
});
/// 建议:前端在开始黄金三章前,先创建一个草稿小说并将 novelId 放入 request
/// 以便后端在大纲/章节保存后直接绑定会话
/// 开始写作确保novelId并保存当前会话设定
Future<String?> startWriting({required String? sessionId, String? novelId, String? historyId});
// ==================== 历史记录管理 ====================
/// 获取用户的历史记录列表
///
/// 使用用户维度管理,支持按小说过滤
Future<List<Map<String, dynamic>>> getUserHistories({
String? novelId,
int page = 0,
int size = 20,
});
/// 获取历史记录详情
Future<Map<String, dynamic>?> getHistoryDetails({
required String historyId,
});
/// 从历史记录创建编辑会话(增强版)
Future<Map<String, dynamic>> createEditSessionFromHistory({
required String historyId,
required String editReason,
required String modelConfigId,
});
/// 复制历史记录
Future<Map<String, dynamic>> copyHistory({
required String historyId,
required String copyReason,
});
/// 恢复历史记录到小说中
Future<Map<String, dynamic>> restoreHistoryToNovel({
required String historyId,
required String novelId,
});
/// 删除历史记录
Future<void> deleteHistory({
required String historyId,
});
/// 批量删除历史记录
Future<Map<String, dynamic>> batchDeleteHistories({
required List<String> historyIds,
});
/// 统计历史记录数量
Future<int> countUserHistories({
String? novelId,
});
/// 获取节点历史记录
Future<List<Map<String, dynamic>>> getNodeHistories({
required String historyId,
required String nodeId,
int page = 0,
int size = 10,
});
// ==================== 策略管理接口 ====================
/// 创建用户自定义策略
Future<Map<String, dynamic>> createCustomStrategy({
required String name,
required String description,
required String systemPrompt,
required String userPrompt,
required List<Map<String, dynamic>> nodeTemplates,
required int expectedRootNodes,
required int maxDepth,
String? baseStrategyId,
});
/// 基于现有策略创建新策略
Future<Map<String, dynamic>> createStrategyFromBase({
required String baseTemplateId,
required String name,
required String description,
String? systemPrompt,
String? userPrompt,
required Map<String, dynamic> modifications,
});
/// 获取用户的策略列表
Future<List<Map<String, dynamic>>> getUserStrategies({
int page = 0,
int size = 20,
});
/// 获取公开策略列表
Future<List<Map<String, dynamic>>> getPublicStrategies({
String? category,
int page = 0,
int size = 20,
});
/// 获取策略详情
Future<Map<String, dynamic>?> getStrategyDetail({
required String strategyId,
});
/// 更新策略
Future<Map<String, dynamic>> updateStrategy({
required String strategyId,
required String name,
required String description,
String? systemPrompt,
String? userPrompt,
List<Map<String, dynamic>>? nodeTemplates,
int? expectedRootNodes,
int? maxDepth,
});
/// 删除策略
Future<void> deleteStrategy({
required String strategyId,
});
/// 提交策略审核
Future<void> submitStrategyForReview({
required String strategyId,
});
/// 获取待审核策略列表(管理员接口)
Future<List<Map<String, dynamic>>> getPendingStrategies({
int page = 0,
int size = 20,
});
/// 审核策略(管理员接口)
Future<void> reviewStrategy({
required String strategyId,
required String decision,
String? comment,
List<String>? rejectionReasons,
List<String>? improvementSuggestions,
});
// ==================== 工具方法 ====================
/// 检查会话是否已关联历史记录
bool isSessionLinkedToHistory(SettingGenerationSession session) {
return session.historyId != null && session.historyId!.isNotEmpty;
}
}

View File

@@ -0,0 +1,28 @@
import 'dart:typed_data';
abstract class StorageRepository {
/// 获取封面上传凭证
Future<Map<String, dynamic>> getCoverUploadCredential({
required String novelId,
required String fileName,
String? contentType,
});
/// 上传封面图片
Future<String> uploadCoverImage({
required String novelId,
required Uint8List fileBytes,
required String fileName,
String? contentType,
bool updateNovelCover = true,
});
/// 获取文件访问URL
Future<String> getFileAccessUrl({
required String fileKey,
int? expirationSeconds,
});
/// 检查用户是否有有效的上传配置
Future<bool> hasValidStorageConfig();
}

View File

@@ -0,0 +1,75 @@
import '../../../models/admin/subscription_models.dart';
import 'package:ainoval/services/api_service/base/api_client.dart';
import 'package:ainoval/utils/logger.dart';
/// 订阅管理仓库接口
abstract interface class SubscriptionRepository {
/// 获取所有订阅计划
Future<List<SubscriptionPlan>> getAllPlans();
/// 获取单个订阅计划
Future<SubscriptionPlan> getPlanById(String id);
/// 创建订阅计划
Future<SubscriptionPlan> createPlan(SubscriptionPlan plan);
/// 更新订阅计划
Future<SubscriptionPlan> updatePlan(String id, SubscriptionPlan plan);
/// 删除订阅计划
Future<void> deletePlan(String id);
/// 切换订阅计划状态
Future<SubscriptionPlan> togglePlanStatus(String id, bool active);
/// 获取订阅统计信息
Future<SubscriptionStatistics> getSubscriptionStatistics();
/// 获取用户订阅历史
Future<List<UserSubscription>> getUserSubscriptions(String userId);
/// 获取活跃的用户订阅
Future<UserSubscription?> getActiveUserSubscription(String userId);
}
/// 面向用户端的公开计划仓库
class PublicSubscriptionRepository {
final ApiClient _apiClient;
static const String _tag = 'PublicSubscriptionRepository';
PublicSubscriptionRepository({ApiClient? apiClient}) : _apiClient = apiClient ?? ApiClient();
Future<List<SubscriptionPlan>> listActivePlans() async {
final res = await _apiClient.get('/subscription-plans');
AppLogger.d(_tag, '订阅计划原始响应类型: ${res.runtimeType}');
AppLogger.d(_tag, '订阅计划原始响应内容: $res');
// 兼容两种返回结构:
// 1) { success, data: [...] }
// 2) 直接返回数组 [...]
if (res is Map<String, dynamic>) {
final data = res['data'];
if (data is List) {
return data
.whereType<Map<String, dynamic>>()
.map(SubscriptionPlan.fromJson)
.toList();
}
} else if (res is List) {
return res
.whereType<Map<String, dynamic>>()
.map(SubscriptionPlan.fromJson)
.toList();
}
AppLogger.w(_tag, '订阅计划响应结构非预期,返回空数组');
// 非预期结构时返回空数组避免UI崩溃
return [];
}
Future<List<Map<String, dynamic>>> listActiveCreditPacks() async {
final res = await _apiClient.get('/credit-packs');
if (res is Map<String, dynamic> && res['data'] is List) {
return (res['data'] as List).cast<Map<String, dynamic>>();
}
return [];
}
}

View File

@@ -0,0 +1,17 @@
import 'package:ainoval/models/ai_request_models.dart';
/// 通用AI请求仓库接口
abstract class UniversalAIRepository {
/// 发送通用AI请求非流式
Future<UniversalAIResponse> sendRequest(UniversalAIRequest request);
/// 发送通用AI请求流式
Stream<UniversalAIResponse> streamRequest(UniversalAIRequest request);
/// 预览请求获取构建的提示内容不实际发送给AI
Future<UniversalAIPreviewResponse> previewRequest(UniversalAIRequest request);
/// 🚀 新增:预估积分成本
/// 快速预估AI请求的积分消耗不实际发送给AI
Future<CostEstimationResponse> estimateCost(UniversalAIRequest request);
}

View File

@@ -0,0 +1,74 @@
import 'dart:async';
import '../../../models/user_ai_model_config_model.dart';
import 'package:ainoval/blocs/ai_config/ai_config_bloc.dart'; // 导入以获取ModelListingCapability枚举
import 'package:ainoval/models/model_info.dart'; // Import ModelInfo
/// 用户 AI 模型配置仓库接口定义
abstract interface class UserAIModelConfigRepository {
/// 获取系统支持的所有AI提供商
Future<List<String>> listAvailableProviders();
/// 获取指定提供商支持的模型列表 (现在返回详细信息)
Future<List<ModelInfo>> listModelsForProvider(String provider);
/// 添加新的用户AI模型配置
Future<UserAIModelConfigModel> addConfiguration({
required String userId,
required String provider,
required String modelName,
String? alias,
required String apiKey,
String? apiEndpoint,
});
/// 列出用户所有的AI模型配置包含解密后的API密钥
/// [validatedOnly] 为 true 时,只返回已验证的配置
Future<List<UserAIModelConfigModel>> listConfigurations({
required String userId,
bool? validatedOnly,
});
/// 获取指定ID的用户AI模型配置
Future<UserAIModelConfigModel> getConfigurationById({
required String userId,
required String configId,
});
/// 更新指定ID的用户AI模型配置
/// [alias], [apiKey], [apiEndpoint] 可选,只传递需要更新的字段
Future<UserAIModelConfigModel> updateConfiguration({
required String userId,
required String configId,
String? alias,
String? apiKey,
String? apiEndpoint,
});
/// 删除指定ID的用户AI模型配置
Future<void> deleteConfiguration({
required String userId,
required String configId,
});
/// 手动触发指定配置的API Key验证
Future<UserAIModelConfigModel> validateConfiguration({
required String userId,
required String configId,
});
/// 设置指定配置为用户的默认模型
Future<UserAIModelConfigModel> setDefaultConfiguration({
required String userId,
required String configId,
});
/// 获取提供商的模型列表能力
Future<ModelListingCapability> getProviderCapability(String providerName);
/// 使用API密钥获取指定提供商的模型列表 (现在返回详细信息)
Future<List<ModelInfo>> listModelsWithApiKey({
required String provider,
required String apiKey,
String? apiEndpoint
});
}