马良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,85 @@
import 'package:equatable/equatable.dart';
/// 管理员认证请求
class AdminAuthRequest extends Equatable {
final String username;
final String password;
const AdminAuthRequest({
required this.username,
required this.password,
});
factory AdminAuthRequest.fromJson(Map<String, dynamic> json) {
return AdminAuthRequest(
username: json['username'] as String,
password: json['password'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'username': username,
'password': password,
};
}
@override
List<Object?> get props => [username, password];
}
/// 管理员认证响应
class AdminAuthResponse extends Equatable {
final String token;
final String refreshToken;
final String userId;
final String username;
final String? displayName;
final List<String> roles;
final List<String> permissions;
const AdminAuthResponse({
required this.token,
required this.refreshToken,
required this.userId,
required this.username,
this.displayName,
required this.roles,
required this.permissions,
});
factory AdminAuthResponse.fromJson(Map<String, dynamic> json) {
return AdminAuthResponse(
token: json['token'] as String,
refreshToken: json['refreshToken'] as String,
userId: json['userId'] as String,
username: json['username'] as String,
displayName: json['displayName'] as String?,
roles: List<String>.from(json['roles'] as List? ?? []),
permissions: List<String>.from(json['permissions'] as List? ?? []),
);
}
Map<String, dynamic> toJson() {
return {
'token': token,
'refreshToken': refreshToken,
'userId': userId,
'username': username,
'displayName': displayName,
'roles': roles,
'permissions': permissions,
};
}
@override
List<Object?> get props => [
token,
refreshToken,
userId,
username,
displayName,
roles,
permissions,
];
}

View File

@@ -0,0 +1,295 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import '../../utils/date_time_parser.dart';
part 'admin_models.g.dart';
@JsonSerializable()
class AdminDashboardStats extends Equatable {
final int totalUsers;
final int activeUsers;
final int totalNovels;
final int aiRequestsToday;
final double creditsConsumed;
final List<ChartData> userGrowthData;
final List<ChartData> requestsData;
final List<ActivityItem> recentActivities;
const AdminDashboardStats({
required this.totalUsers,
required this.activeUsers,
required this.totalNovels,
required this.aiRequestsToday,
required this.creditsConsumed,
required this.userGrowthData,
required this.requestsData,
required this.recentActivities,
});
factory AdminDashboardStats.fromJson(Map<String, dynamic> json) =>
_$AdminDashboardStatsFromJson(json);
Map<String, dynamic> toJson() => _$AdminDashboardStatsToJson(this);
@override
List<Object> get props => [
totalUsers,
activeUsers,
totalNovels,
aiRequestsToday,
creditsConsumed,
userGrowthData,
requestsData,
recentActivities,
];
}
@JsonSerializable()
class ChartData extends Equatable {
final String label;
final double value;
final DateTime date;
const ChartData({
required this.label,
required this.value,
required this.date,
});
factory ChartData.fromJson(Map<String, dynamic> json) {
return ChartData(
label: json['label'] as String,
value: (json['value'] as num).toDouble(),
date: parseBackendDateTime(json['date']),
);
}
Map<String, dynamic> toJson() => _$ChartDataToJson(this);
@override
List<Object> get props => [label, value, date];
}
@JsonSerializable()
class ActivityItem extends Equatable {
final String id;
final String userId;
final String userName;
final String action;
final String description;
final DateTime timestamp;
final String? metadata;
const ActivityItem({
required this.id,
required this.userId,
required this.userName,
required this.action,
required this.description,
required this.timestamp,
this.metadata,
});
factory ActivityItem.fromJson(Map<String, dynamic> json) {
return ActivityItem(
id: json['id'] as String,
userId: json['userId'] as String,
userName: json['userName'] as String,
action: json['action'] as String,
description: json['description'] as String,
timestamp: parseBackendDateTime(json['timestamp']),
metadata: json['metadata'] as String?,
);
}
Map<String, dynamic> toJson() => _$ActivityItemToJson(this);
@override
List<Object?> get props => [
id,
userId,
userName,
action,
description,
timestamp,
metadata,
];
}
@JsonSerializable()
class AdminUser extends Equatable {
final String id;
final String username;
final String email; // 后端可能返回 null这里统一转换为空串
final String? displayName;
final String accountStatus;
final int credits;
final List<String> roles;
final DateTime createdAt;
final DateTime? updatedAt;
const AdminUser({
required this.id,
required this.username,
required this.email,
this.displayName,
required this.accountStatus,
required this.credits,
required this.roles,
required this.createdAt,
this.updatedAt,
});
factory AdminUser.fromJson(Map<String, dynamic> json) {
return AdminUser(
id: json['id'] as String,
username: json['username'] as String,
email: (json['email'] as String?) ?? '',
displayName: json['displayName'] as String?,
accountStatus: json['accountStatus']?.toString() ?? 'ACTIVE',
credits: (json['credits'] as num?)?.toInt() ?? 0,
roles: (json['roles'] as List?)?.map((e) => e.toString()).toList() ?? [],
createdAt: parseBackendDateTime(json['createdAt']),
updatedAt: json['updatedAt'] != null ? parseBackendDateTime(json['updatedAt']) : null,
);
}
Map<String, dynamic> toJson() => _$AdminUserToJson(this);
@override
List<Object?> get props => [
id,
username,
email,
displayName,
accountStatus,
credits,
roles,
createdAt,
updatedAt,
];
}
@JsonSerializable()
class AdminRole extends Equatable {
final String? id;
final String roleName;
final String displayName;
final String? description;
final List<String> permissions;
final bool enabled;
final int priority;
const AdminRole({
this.id,
required this.roleName,
required this.displayName,
this.description,
required this.permissions,
required this.enabled,
required this.priority,
});
factory AdminRole.fromJson(Map<String, dynamic> json) =>
_$AdminRoleFromJson(json);
Map<String, dynamic> toJson() => _$AdminRoleToJson(this);
@override
List<Object?> get props => [
id,
roleName,
displayName,
description,
permissions,
enabled,
priority,
];
}
@JsonSerializable()
class AdminModelConfig extends Equatable {
final String? id;
final String provider;
final String modelId;
final String? displayName;
final bool enabled;
final List<String> enabledForFeatures;
final double creditRateMultiplier;
final int maxConcurrentRequests;
final int dailyRequestLimit;
final String? description;
const AdminModelConfig({
this.id,
required this.provider,
required this.modelId,
this.displayName,
required this.enabled,
required this.enabledForFeatures,
required this.creditRateMultiplier,
required this.maxConcurrentRequests,
required this.dailyRequestLimit,
this.description,
});
factory AdminModelConfig.fromJson(Map<String, dynamic> json) =>
_$AdminModelConfigFromJson(json);
Map<String, dynamic> toJson() => _$AdminModelConfigToJson(this);
@override
List<Object?> get props => [
id,
provider,
modelId,
displayName,
enabled,
enabledForFeatures,
creditRateMultiplier,
maxConcurrentRequests,
dailyRequestLimit,
description,
];
}
@JsonSerializable()
class AdminSystemConfig extends Equatable {
final String id;
final String configKey;
final String configValue;
final String? description;
final String configType;
final String? configGroup;
final bool enabled;
final bool readOnly;
const AdminSystemConfig({
required this.id,
required this.configKey,
required this.configValue,
this.description,
required this.configType,
this.configGroup,
required this.enabled,
required this.readOnly,
});
factory AdminSystemConfig.fromJson(Map<String, dynamic> json) =>
_$AdminSystemConfigFromJson(json);
Map<String, dynamic> toJson() => _$AdminSystemConfigToJson(this);
@override
List<Object?> get props => [
id,
configKey,
configValue,
description,
configType,
configGroup,
enabled,
readOnly,
];
}

View File

@@ -0,0 +1,282 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'admin_models.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AdminDashboardStats _$AdminDashboardStatsFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'AdminDashboardStats',
json,
($checkedConvert) {
final val = AdminDashboardStats(
totalUsers: $checkedConvert('totalUsers', (v) => (v as num).toInt()),
activeUsers:
$checkedConvert('activeUsers', (v) => (v as num).toInt()),
totalNovels:
$checkedConvert('totalNovels', (v) => (v as num).toInt()),
aiRequestsToday:
$checkedConvert('aiRequestsToday', (v) => (v as num).toInt()),
creditsConsumed:
$checkedConvert('creditsConsumed', (v) => (v as num).toDouble()),
userGrowthData: $checkedConvert(
'userGrowthData',
(v) => (v as List<dynamic>)
.map((e) => ChartData.fromJson(e as Map<String, dynamic>))
.toList()),
requestsData: $checkedConvert(
'requestsData',
(v) => (v as List<dynamic>)
.map((e) => ChartData.fromJson(e as Map<String, dynamic>))
.toList()),
recentActivities: $checkedConvert(
'recentActivities',
(v) => (v as List<dynamic>)
.map((e) => ActivityItem.fromJson(e as Map<String, dynamic>))
.toList()),
);
return val;
},
);
Map<String, dynamic> _$AdminDashboardStatsToJson(
AdminDashboardStats instance) =>
<String, dynamic>{
'totalUsers': instance.totalUsers,
'activeUsers': instance.activeUsers,
'totalNovels': instance.totalNovels,
'aiRequestsToday': instance.aiRequestsToday,
'creditsConsumed': instance.creditsConsumed,
'userGrowthData': instance.userGrowthData.map((e) => e.toJson()).toList(),
'requestsData': instance.requestsData.map((e) => e.toJson()).toList(),
'recentActivities':
instance.recentActivities.map((e) => e.toJson()).toList(),
};
ChartData _$ChartDataFromJson(Map<String, dynamic> json) => $checkedCreate(
'ChartData',
json,
($checkedConvert) {
final val = ChartData(
label: $checkedConvert('label', (v) => v as String),
value: $checkedConvert('value', (v) => (v as num).toDouble()),
date: $checkedConvert('date', (v) => DateTime.parse(v as String)),
);
return val;
},
);
Map<String, dynamic> _$ChartDataToJson(ChartData instance) => <String, dynamic>{
'label': instance.label,
'value': instance.value,
'date': instance.date.toIso8601String(),
};
ActivityItem _$ActivityItemFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'ActivityItem',
json,
($checkedConvert) {
final val = ActivityItem(
id: $checkedConvert('id', (v) => v as String),
userId: $checkedConvert('userId', (v) => v as String),
userName: $checkedConvert('userName', (v) => v as String),
action: $checkedConvert('action', (v) => v as String),
description: $checkedConvert('description', (v) => v as String),
timestamp:
$checkedConvert('timestamp', (v) => DateTime.parse(v as String)),
metadata: $checkedConvert('metadata', (v) => v as String?),
);
return val;
},
);
Map<String, dynamic> _$ActivityItemToJson(ActivityItem instance) {
final val = <String, dynamic>{
'id': instance.id,
'userId': instance.userId,
'userName': instance.userName,
'action': instance.action,
'description': instance.description,
'timestamp': instance.timestamp.toIso8601String(),
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('metadata', instance.metadata);
return val;
}
AdminUser _$AdminUserFromJson(Map<String, dynamic> json) => $checkedCreate(
'AdminUser',
json,
($checkedConvert) {
final val = AdminUser(
id: $checkedConvert('id', (v) => v as String),
username: $checkedConvert('username', (v) => v as String),
email: $checkedConvert('email', (v) => v as String),
displayName: $checkedConvert('displayName', (v) => v as String?),
accountStatus: $checkedConvert('accountStatus', (v) => v as String),
credits: $checkedConvert('credits', (v) => (v as num).toInt()),
roles: $checkedConvert('roles',
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
createdAt:
$checkedConvert('createdAt', (v) => DateTime.parse(v as String)),
updatedAt: $checkedConvert('updatedAt',
(v) => v == null ? null : DateTime.parse(v as String)),
);
return val;
},
);
Map<String, dynamic> _$AdminUserToJson(AdminUser instance) {
final val = <String, dynamic>{
'id': instance.id,
'username': instance.username,
'email': instance.email,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('displayName', instance.displayName);
val['accountStatus'] = instance.accountStatus;
val['credits'] = instance.credits;
val['roles'] = instance.roles;
val['createdAt'] = instance.createdAt.toIso8601String();
writeNotNull('updatedAt', instance.updatedAt?.toIso8601String());
return val;
}
AdminRole _$AdminRoleFromJson(Map<String, dynamic> json) => $checkedCreate(
'AdminRole',
json,
($checkedConvert) {
final val = AdminRole(
id: $checkedConvert('id', (v) => v as String?),
roleName: $checkedConvert('roleName', (v) => v as String),
displayName: $checkedConvert('displayName', (v) => v as String),
description: $checkedConvert('description', (v) => v as String?),
permissions: $checkedConvert('permissions',
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
enabled: $checkedConvert('enabled', (v) => v as bool),
priority: $checkedConvert('priority', (v) => (v as num).toInt()),
);
return val;
},
);
Map<String, dynamic> _$AdminRoleToJson(AdminRole instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('id', instance.id);
val['roleName'] = instance.roleName;
val['displayName'] = instance.displayName;
writeNotNull('description', instance.description);
val['permissions'] = instance.permissions;
val['enabled'] = instance.enabled;
val['priority'] = instance.priority;
return val;
}
AdminModelConfig _$AdminModelConfigFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'AdminModelConfig',
json,
($checkedConvert) {
final val = AdminModelConfig(
id: $checkedConvert('id', (v) => v as String?),
provider: $checkedConvert('provider', (v) => v as String),
modelId: $checkedConvert('modelId', (v) => v as String),
displayName: $checkedConvert('displayName', (v) => v as String?),
enabled: $checkedConvert('enabled', (v) => v as bool),
enabledForFeatures: $checkedConvert('enabledForFeatures',
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
creditRateMultiplier: $checkedConvert(
'creditRateMultiplier', (v) => (v as num).toDouble()),
maxConcurrentRequests: $checkedConvert(
'maxConcurrentRequests', (v) => (v as num).toInt()),
dailyRequestLimit:
$checkedConvert('dailyRequestLimit', (v) => (v as num).toInt()),
description: $checkedConvert('description', (v) => v as String?),
);
return val;
},
);
Map<String, dynamic> _$AdminModelConfigToJson(AdminModelConfig instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('id', instance.id);
val['provider'] = instance.provider;
val['modelId'] = instance.modelId;
writeNotNull('displayName', instance.displayName);
val['enabled'] = instance.enabled;
val['enabledForFeatures'] = instance.enabledForFeatures;
val['creditRateMultiplier'] = instance.creditRateMultiplier;
val['maxConcurrentRequests'] = instance.maxConcurrentRequests;
val['dailyRequestLimit'] = instance.dailyRequestLimit;
writeNotNull('description', instance.description);
return val;
}
AdminSystemConfig _$AdminSystemConfigFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'AdminSystemConfig',
json,
($checkedConvert) {
final val = AdminSystemConfig(
id: $checkedConvert('id', (v) => v as String),
configKey: $checkedConvert('configKey', (v) => v as String),
configValue: $checkedConvert('configValue', (v) => v as String),
description: $checkedConvert('description', (v) => v as String?),
configType: $checkedConvert('configType', (v) => v as String),
configGroup: $checkedConvert('configGroup', (v) => v as String?),
enabled: $checkedConvert('enabled', (v) => v as bool),
readOnly: $checkedConvert('readOnly', (v) => v as bool),
);
return val;
},
);
Map<String, dynamic> _$AdminSystemConfigToJson(AdminSystemConfig instance) {
final val = <String, dynamic>{
'id': instance.id,
'configKey': instance.configKey,
'configValue': instance.configValue,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('description', instance.description);
val['configType'] = instance.configType;
writeNotNull('configGroup', instance.configGroup);
val['enabled'] = instance.enabled;
val['readOnly'] = instance.readOnly;
return val;
}

View File

@@ -0,0 +1,54 @@
class CreditTransactionModel {
final String traceId;
final String? userId;
final String? provider;
final String? modelId;
final String? featureType;
final int? inputTokens;
final int? outputTokens;
final int? creditsDeducted;
final String status; // PENDING, DEDUCTED, FAILED, COMPENSATED
final String? errorMessage;
final String? reversalOfTraceId;
final String? operatorUserId;
final String? auditNote;
final String? createdAt; // ISO8601 from backend
CreditTransactionModel({
required this.traceId,
required this.status,
this.userId,
this.provider,
this.modelId,
this.featureType,
this.inputTokens,
this.outputTokens,
this.creditsDeducted,
this.errorMessage,
this.reversalOfTraceId,
this.operatorUserId,
this.auditNote,
this.createdAt,
});
factory CreditTransactionModel.fromJson(Map<String, dynamic> json) {
return CreditTransactionModel(
traceId: (json['traceId'] ?? '').toString(),
userId: json['userId']?.toString(),
provider: json['provider']?.toString(),
modelId: json['modelId']?.toString(),
featureType: json['featureType']?.toString(),
inputTokens: json['inputTokens'] is int ? json['inputTokens'] as int : int.tryParse('${json['inputTokens'] ?? ''}'),
outputTokens: json['outputTokens'] is int ? json['outputTokens'] as int : int.tryParse('${json['outputTokens'] ?? ''}'),
creditsDeducted: json['creditsDeducted'] is int ? json['creditsDeducted'] as int : int.tryParse('${json['creditsDeducted'] ?? ''}'),
status: (json['status'] ?? '').toString(),
errorMessage: json['errorMessage']?.toString(),
reversalOfTraceId: json['reversalOfTraceId']?.toString(),
operatorUserId: json['operatorUserId']?.toString(),
auditNote: json['auditNote']?.toString(),
createdAt: json['createdAt']?.toString(),
);
}
}

View File

@@ -0,0 +1,702 @@
/// LLM可观测性相关数据模型
/// 用于管理后台查看和分析大模型调用日志
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import '../../utils/date_time_parser.dart';
part 'llm_observability_models.g.dart';
/// 自定义时间戳转换器
class TimestampConverter implements JsonConverter<DateTime, dynamic> {
const TimestampConverter();
@override
DateTime fromJson(dynamic timestamp) {
return parseBackendDateTime(timestamp);
}
@override
dynamic toJson(DateTime timestamp) {
return timestamp.toIso8601String();
}
}
/// LLM调用日志
@JsonSerializable()
class LLMTrace extends Equatable {
final String id;
final String traceId;
final String provider;
final String model;
final String? userId;
final String? sessionId;
@JsonKey(name: 'createdAt')
@TimestampConverter()
final DateTime timestamp;
// 请求信息
final LLMRequest request;
// 响应信息
final LLMResponse? response;
// 性能指标
final LLMPerformanceMetrics? performance;
// 错误信息
final LLMError? error;
// 工具调用
final List<LLMToolCall>? toolCalls;
// 元数据
final Map<String, dynamic>? metadata;
// 状态
@JsonKey(defaultValue: LLMTraceStatus.pending)
final LLMTraceStatus status;
@JsonKey(defaultValue: false)
final bool isStreaming;
const LLMTrace({
required this.id,
required this.traceId,
required this.provider,
required this.model,
this.userId,
this.sessionId,
required this.timestamp,
required this.request,
this.response,
this.performance,
this.error,
this.toolCalls,
this.metadata,
this.status = LLMTraceStatus.pending,
this.isStreaming = false,
});
factory LLMTrace.fromJson(Map<String, dynamic> json) => _$LLMTraceFromJson(json);
Map<String, dynamic> toJson() => _$LLMTraceToJson(this);
@override
List<Object?> get props => [id, traceId, provider, model, userId, sessionId, timestamp, request, response, performance, error, toolCalls, metadata, status, isStreaming];
}
/// LLM请求信息
@JsonSerializable()
class LLMRequest extends Equatable {
final List<LLMMessage>? messages;
// 模型参数
final double? temperature;
final double? topP;
final int? topK;
final int? maxTokens;
final int? seed;
// 工具调用
final List<LLMTool>? tools;
final String? toolChoice;
// 格式设置
final String? responseFormat;
// 其他参数
final Map<String, dynamic>? additionalParameters;
const LLMRequest({
this.messages,
this.temperature,
this.topP,
this.topK,
this.maxTokens,
this.seed,
this.tools,
this.toolChoice,
this.responseFormat,
this.additionalParameters,
});
factory LLMRequest.fromJson(Map<String, dynamic> json) => _$LLMRequestFromJson(json);
Map<String, dynamic> toJson() => _$LLMRequestToJson(this);
@override
List<Object?> get props => [messages, temperature, topP, topK, maxTokens, seed, tools, toolChoice, responseFormat, additionalParameters];
}
/// LLM响应信息
@JsonSerializable()
class LLMResponse extends Equatable {
final String? id;
final String? content;
// Token使用情况
final LLMTokenUsage? tokenUsage;
// 完成原因
final String? finishReason;
// 工具调用结果
final List<LLMToolCallResult>? toolCallResults;
// 元数据
final Map<String, dynamic>? metadata;
// 流式数据
final List<String>? streamChunks;
const LLMResponse({
this.id,
this.content,
this.tokenUsage,
this.finishReason,
this.toolCallResults,
this.metadata,
this.streamChunks,
});
factory LLMResponse.fromJson(Map<String, dynamic> json) => _$LLMResponseFromJson(json);
Map<String, dynamic> toJson() => _$LLMResponseToJson(this);
@override
List<Object?> get props => [id, content, tokenUsage, finishReason, toolCallResults, metadata, streamChunks];
}
/// LLM消息
@JsonSerializable()
class LLMMessage extends Equatable {
final String role;
final String? content;
final String? name;
final Map<String, dynamic>? metadata;
const LLMMessage({
required this.role,
this.content,
this.name,
this.metadata,
});
factory LLMMessage.fromJson(Map<String, dynamic> json) => _$LLMMessageFromJson(json);
Map<String, dynamic> toJson() => _$LLMMessageToJson(this);
@override
List<Object?> get props => [role, content, name, metadata];
}
/// LLM工具定义
@JsonSerializable()
class LLMTool extends Equatable {
final String name;
final String? description;
final Map<String, dynamic>? parameters;
const LLMTool({
required this.name,
this.description,
this.parameters,
});
factory LLMTool.fromJson(Map<String, dynamic> json) => _$LLMToolFromJson(json);
Map<String, dynamic> toJson() => _$LLMToolToJson(this);
@override
List<Object?> get props => [name, description, parameters];
}
/// LLM工具调用
@JsonSerializable()
class LLMToolCall extends Equatable {
final String id;
final String name;
final Map<String, dynamic>? arguments;
final DateTime? timestamp;
const LLMToolCall({
required this.id,
required this.name,
this.arguments,
this.timestamp,
});
factory LLMToolCall.fromJson(Map<String, dynamic> json) => _$LLMToolCallFromJson(json);
Map<String, dynamic> toJson() => _$LLMToolCallToJson(this);
@override
List<Object?> get props => [id, name, arguments, timestamp];
}
/// LLM工具调用结果
@JsonSerializable()
class LLMToolCallResult extends Equatable {
final String toolCallId;
final String? result;
final LLMError? error;
const LLMToolCallResult({
required this.toolCallId,
this.result,
this.error,
});
factory LLMToolCallResult.fromJson(Map<String, dynamic> json) => _$LLMToolCallResultFromJson(json);
Map<String, dynamic> toJson() => _$LLMToolCallResultToJson(this);
@override
List<Object?> get props => [toolCallId, result, error];
}
/// Token使用情况
@JsonSerializable()
class LLMTokenUsage extends Equatable {
final int? promptTokens;
final int? completionTokens;
final int? totalTokens;
// 详细分解
final int? inputTokens;
final int? outputTokens;
final int? reasoningTokens;
final int? cachedTokens;
const LLMTokenUsage({
this.promptTokens,
this.completionTokens,
this.totalTokens,
this.inputTokens,
this.outputTokens,
this.reasoningTokens,
this.cachedTokens,
});
factory LLMTokenUsage.fromJson(Map<String, dynamic> json) => _$LLMTokenUsageFromJson(json);
Map<String, dynamic> toJson() => _$LLMTokenUsageToJson(this);
@override
List<Object?> get props => [promptTokens, completionTokens, totalTokens, inputTokens, outputTokens, reasoningTokens, cachedTokens];
}
/// 性能指标
@JsonSerializable()
class LLMPerformanceMetrics extends Equatable {
final int? requestLatencyMs;
final int? firstTokenLatencyMs;
final int? totalDurationMs;
// 吞吐量
final double? tokensPerSecond;
final double? charactersPerSecond;
// 队列时间
final int? queueTimeMs;
final int? processingTimeMs;
const LLMPerformanceMetrics({
this.requestLatencyMs,
this.firstTokenLatencyMs,
this.totalDurationMs,
this.tokensPerSecond,
this.charactersPerSecond,
this.queueTimeMs,
this.processingTimeMs,
});
factory LLMPerformanceMetrics.fromJson(Map<String, dynamic> json) => _$LLMPerformanceMetricsFromJson(json);
Map<String, dynamic> toJson() => _$LLMPerformanceMetricsToJson(this);
@override
List<Object?> get props => [requestLatencyMs, firstTokenLatencyMs, totalDurationMs, tokensPerSecond, charactersPerSecond, queueTimeMs, processingTimeMs];
}
/// 错误信息
@JsonSerializable()
class LLMError extends Equatable {
final String? type;
final String? message;
final String? code;
final String? stackTrace;
final Map<String, dynamic>? details;
const LLMError({
this.type,
this.message,
this.code,
this.stackTrace,
this.details,
});
factory LLMError.fromJson(Map<String, dynamic> json) => _$LLMErrorFromJson(json);
Map<String, dynamic> toJson() => _$LLMErrorToJson(this);
@override
List<Object?> get props => [type, message, code, stackTrace, details];
}
/// LLM调用状态
enum LLMTraceStatus {
@JsonValue('pending')
pending,
@JsonValue('success')
success,
@JsonValue('error')
error,
@JsonValue('timeout')
timeout,
@JsonValue('cancelled')
cancelled,
}
/// 统计信息基类
@JsonSerializable()
class LLMStatistics extends Equatable {
final int totalCalls;
final int successfulCalls;
final int failedCalls;
final double successRate;
final double averageLatency;
final int totalTokens;
// 时间范围
final DateTime? startTime;
final DateTime? endTime;
// 详细统计
final Map<String, dynamic>? details;
const LLMStatistics({
required this.totalCalls,
required this.successfulCalls,
required this.failedCalls,
required this.successRate,
required this.averageLatency,
required this.totalTokens,
this.startTime,
this.endTime,
this.details,
});
factory LLMStatistics.fromJson(Map<String, dynamic> json) => _$LLMStatisticsFromJson(json);
Map<String, dynamic> toJson() => _$LLMStatisticsToJson(this);
@override
List<Object?> get props => [totalCalls, successfulCalls, failedCalls, successRate, averageLatency, totalTokens, startTime, endTime, details];
}
/// 提供商统计
@JsonSerializable()
class ProviderStatistics extends Equatable {
final String provider;
final LLMStatistics statistics;
final List<ModelStatistics> models;
const ProviderStatistics({
required this.provider,
required this.statistics,
required this.models,
});
factory ProviderStatistics.fromJson(Map<String, dynamic> json) => _$ProviderStatisticsFromJson(json);
Map<String, dynamic> toJson() => _$ProviderStatisticsToJson(this);
@override
List<Object?> get props => [provider, statistics, models];
}
/// 模型统计
@JsonSerializable()
class ModelStatistics extends Equatable {
final String modelName;
final String provider;
final LLMStatistics statistics;
const ModelStatistics({
required this.modelName,
required this.provider,
required this.statistics,
});
factory ModelStatistics.fromJson(Map<String, dynamic> json) => _$ModelStatisticsFromJson(json);
Map<String, dynamic> toJson() => _$ModelStatisticsToJson(this);
@override
List<Object?> get props => [modelName, provider, statistics];
}
/// 用户统计
@JsonSerializable()
class UserStatistics extends Equatable {
final String userId;
final String? username;
final LLMStatistics statistics;
final List<String> topModels;
final List<String> topProviders;
const UserStatistics({
required this.userId,
this.username,
required this.statistics,
required this.topModels,
required this.topProviders,
});
factory UserStatistics.fromJson(Map<String, dynamic> json) => _$UserStatisticsFromJson(json);
Map<String, dynamic> toJson() => _$UserStatisticsToJson(this);
@override
List<Object?> get props => [userId, username, statistics, topModels, topProviders];
}
/// 错误统计
@JsonSerializable()
class ErrorStatistics extends Equatable {
final String errorType;
final int count;
final double percentage;
final List<String> topErrorMessages;
final List<String> affectedModels;
const ErrorStatistics({
required this.errorType,
required this.count,
required this.percentage,
required this.topErrorMessages,
required this.affectedModels,
});
factory ErrorStatistics.fromJson(Map<String, dynamic> json) => _$ErrorStatisticsFromJson(json);
Map<String, dynamic> toJson() => _$ErrorStatisticsToJson(this);
@override
List<Object?> get props => [errorType, count, percentage, topErrorMessages, affectedModels];
}
/// 性能统计
@JsonSerializable()
class PerformanceStatistics extends Equatable {
final double averageLatency;
final double medianLatency;
final double p95Latency;
final double p99Latency;
final double averageThroughput;
// 按时间分组的统计
final List<TimeBasedMetric> latencyTrends;
final List<TimeBasedMetric> throughputTrends;
const PerformanceStatistics({
required this.averageLatency,
required this.medianLatency,
required this.p95Latency,
required this.p99Latency,
required this.averageThroughput,
required this.latencyTrends,
required this.throughputTrends,
});
factory PerformanceStatistics.fromJson(Map<String, dynamic> json) => _$PerformanceStatisticsFromJson(json);
Map<String, dynamic> toJson() => _$PerformanceStatisticsToJson(this);
@override
List<Object?> get props => [averageLatency, medianLatency, p95Latency, p99Latency, averageThroughput, latencyTrends, throughputTrends];
}
/// 基于时间的指标
@JsonSerializable()
class TimeBasedMetric extends Equatable {
final DateTime timestamp;
final double value;
final String? label;
const TimeBasedMetric({
required this.timestamp,
required this.value,
this.label,
});
factory TimeBasedMetric.fromJson(Map<String, dynamic> json) => _$TimeBasedMetricFromJson(json);
Map<String, dynamic> toJson() => _$TimeBasedMetricToJson(this);
@override
List<Object?> get props => [timestamp, value, label];
}
/// 系统健康状态
@JsonSerializable()
class SystemHealthStatus extends Equatable {
@JsonKey(defaultValue: HealthStatus.healthy)
final HealthStatus status;
final Map<String, ComponentHealth> components;
final String? message;
final DateTime? lastChecked;
const SystemHealthStatus({
this.status = HealthStatus.healthy,
required this.components,
this.message,
this.lastChecked,
});
factory SystemHealthStatus.fromJson(Map<String, dynamic> json) => _$SystemHealthStatusFromJson(json);
Map<String, dynamic> toJson() => _$SystemHealthStatusToJson(this);
@override
List<Object?> get props => [status, components, message, lastChecked];
}
/// 组件健康状态
@JsonSerializable()
class ComponentHealth extends Equatable {
@JsonKey(defaultValue: HealthStatus.healthy)
final HealthStatus status;
final String? message;
final Map<String, dynamic>? metrics;
const ComponentHealth({
this.status = HealthStatus.healthy,
this.message,
this.metrics,
});
factory ComponentHealth.fromJson(Map<String, dynamic> json) => _$ComponentHealthFromJson(json);
Map<String, dynamic> toJson() => _$ComponentHealthToJson(this);
@override
List<Object?> get props => [status, message, metrics];
}
/// 健康状态枚举
enum HealthStatus {
@JsonValue('healthy')
healthy,
@JsonValue('degraded')
degraded,
@JsonValue('unhealthy')
unhealthy,
@JsonValue('unknown')
unknown,
}
/// LLM日志搜索条件
@JsonSerializable()
class LLMTraceSearchCriteria extends Equatable {
final String? userId;
final String? provider;
final String? model;
final String? sessionId;
final bool? hasError;
final LLMTraceStatus? status;
final DateTime? startTime;
final DateTime? endTime;
// 分页
@JsonKey(defaultValue: 0)
final int page;
@JsonKey(defaultValue: 20)
final int size;
@JsonKey(defaultValue: 'timestamp')
final String sortBy;
@JsonKey(defaultValue: 'desc')
final String sortDir;
const LLMTraceSearchCriteria({
this.userId,
this.provider,
this.model,
this.sessionId,
this.hasError,
this.status,
this.startTime,
this.endTime,
this.page = 0,
this.size = 20,
this.sortBy = 'timestamp',
this.sortDir = 'desc',
});
factory LLMTraceSearchCriteria.fromJson(Map<String, dynamic> json) => _$LLMTraceSearchCriteriaFromJson(json);
Map<String, dynamic> toJson() => _$LLMTraceSearchCriteriaToJson(this);
@override
List<Object?> get props => [userId, provider, model, sessionId, hasError, status, startTime, endTime, page, size, sortBy, sortDir];
}
/// API响应包装类
@JsonSerializable(genericArgumentFactories: true)
class ApiResponse<T> extends Equatable {
final bool success;
final String? message;
final T? data;
final String? error;
const ApiResponse({
required this.success,
this.message,
this.data,
this.error,
});
factory ApiResponse.fromJson(Map<String, dynamic> json, T Function(Object? json) fromJsonT) =>
_$ApiResponseFromJson(json, fromJsonT);
Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$ApiResponseToJson(this, toJsonT);
@override
List<Object?> get props => [success, message, data, error];
}
/// 分页响应
@JsonSerializable(genericArgumentFactories: true)
class PagedResponse<T> extends Equatable {
final List<T> content;
final int page;
final int size;
final int totalElements;
final int totalPages;
@JsonKey(defaultValue: false)
final bool first;
@JsonKey(defaultValue: false)
final bool last;
const PagedResponse({
required this.content,
required this.page,
required this.size,
required this.totalElements,
required this.totalPages,
this.first = false,
this.last = false,
});
factory PagedResponse.fromJson(Map<String, dynamic> json, T Function(Object? json) fromJsonT) =>
_$PagedResponseFromJson(json, fromJsonT);
Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$PagedResponseToJson(this, toJsonT);
@override
List<Object?> get props => [content, page, size, totalElements, totalPages, first, last];
}
/// 游标分页响应
@JsonSerializable(genericArgumentFactories: true)
class CursorPageResponse<T> extends Equatable {
final List<T> items;
final String? nextCursor;
@JsonKey(defaultValue: false)
final bool hasMore;
const CursorPageResponse({
required this.items,
this.nextCursor,
this.hasMore = false,
});
factory CursorPageResponse.fromJson(Map<String, dynamic> json, T Function(Object? json) fromJsonT) =>
_$CursorPageResponseFromJson(json, fromJsonT);
Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$CursorPageResponseToJson(this, toJsonT);
@override
List<Object?> get props => [items, nextCursor, hasMore];
}

View File

@@ -0,0 +1,951 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'llm_observability_models.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
LLMTrace _$LLMTraceFromJson(Map<String, dynamic> json) => $checkedCreate(
'LLMTrace',
json,
($checkedConvert) {
final val = LLMTrace(
id: $checkedConvert('id', (v) => v as String),
traceId: $checkedConvert('traceId', (v) => v as String),
provider: $checkedConvert('provider', (v) => v as String),
model: $checkedConvert('model', (v) => v as String),
userId: $checkedConvert('userId', (v) => v as String?),
sessionId: $checkedConvert('sessionId', (v) => v as String?),
timestamp: $checkedConvert(
'createdAt', (v) => const TimestampConverter().fromJson(v)),
request: $checkedConvert(
'request', (v) => LLMRequest.fromJson(v as Map<String, dynamic>)),
response: $checkedConvert(
'response',
(v) => v == null
? null
: LLMResponse.fromJson(v as Map<String, dynamic>)),
performance: $checkedConvert(
'performance',
(v) => v == null
? null
: LLMPerformanceMetrics.fromJson(v as Map<String, dynamic>)),
error: $checkedConvert(
'error',
(v) => v == null
? null
: LLMError.fromJson(v as Map<String, dynamic>)),
toolCalls: $checkedConvert(
'toolCalls',
(v) => (v as List<dynamic>?)
?.map((e) => LLMToolCall.fromJson(e as Map<String, dynamic>))
.toList()),
metadata:
$checkedConvert('metadata', (v) => v as Map<String, dynamic>?),
status: $checkedConvert(
'status',
(v) =>
$enumDecodeNullable(_$LLMTraceStatusEnumMap, v) ??
LLMTraceStatus.pending),
isStreaming:
$checkedConvert('isStreaming', (v) => v as bool? ?? false),
);
return val;
},
fieldKeyMap: const {'timestamp': 'createdAt'},
);
Map<String, dynamic> _$LLMTraceToJson(LLMTrace instance) {
final val = <String, dynamic>{
'id': instance.id,
'traceId': instance.traceId,
'provider': instance.provider,
'model': instance.model,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('userId', instance.userId);
writeNotNull('sessionId', instance.sessionId);
writeNotNull(
'createdAt', const TimestampConverter().toJson(instance.timestamp));
val['request'] = instance.request.toJson();
writeNotNull('response', instance.response?.toJson());
writeNotNull('performance', instance.performance?.toJson());
writeNotNull('error', instance.error?.toJson());
writeNotNull(
'toolCalls', instance.toolCalls?.map((e) => e.toJson()).toList());
writeNotNull('metadata', instance.metadata);
val['status'] = _$LLMTraceStatusEnumMap[instance.status]!;
val['isStreaming'] = instance.isStreaming;
return val;
}
const _$LLMTraceStatusEnumMap = {
LLMTraceStatus.pending: 'pending',
LLMTraceStatus.success: 'success',
LLMTraceStatus.error: 'error',
LLMTraceStatus.timeout: 'timeout',
LLMTraceStatus.cancelled: 'cancelled',
};
LLMRequest _$LLMRequestFromJson(Map<String, dynamic> json) => $checkedCreate(
'LLMRequest',
json,
($checkedConvert) {
final val = LLMRequest(
messages: $checkedConvert(
'messages',
(v) => (v as List<dynamic>?)
?.map((e) => LLMMessage.fromJson(e as Map<String, dynamic>))
.toList()),
temperature:
$checkedConvert('temperature', (v) => (v as num?)?.toDouble()),
topP: $checkedConvert('topP', (v) => (v as num?)?.toDouble()),
topK: $checkedConvert('topK', (v) => (v as num?)?.toInt()),
maxTokens: $checkedConvert('maxTokens', (v) => (v as num?)?.toInt()),
seed: $checkedConvert('seed', (v) => (v as num?)?.toInt()),
tools: $checkedConvert(
'tools',
(v) => (v as List<dynamic>?)
?.map((e) => LLMTool.fromJson(e as Map<String, dynamic>))
.toList()),
toolChoice: $checkedConvert('toolChoice', (v) => v as String?),
responseFormat:
$checkedConvert('responseFormat', (v) => v as String?),
additionalParameters: $checkedConvert(
'additionalParameters', (v) => v as Map<String, dynamic>?),
);
return val;
},
);
Map<String, dynamic> _$LLMRequestToJson(LLMRequest instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('messages', instance.messages?.map((e) => e.toJson()).toList());
writeNotNull('temperature', instance.temperature);
writeNotNull('topP', instance.topP);
writeNotNull('topK', instance.topK);
writeNotNull('maxTokens', instance.maxTokens);
writeNotNull('seed', instance.seed);
writeNotNull('tools', instance.tools?.map((e) => e.toJson()).toList());
writeNotNull('toolChoice', instance.toolChoice);
writeNotNull('responseFormat', instance.responseFormat);
writeNotNull('additionalParameters', instance.additionalParameters);
return val;
}
LLMResponse _$LLMResponseFromJson(Map<String, dynamic> json) => $checkedCreate(
'LLMResponse',
json,
($checkedConvert) {
final val = LLMResponse(
id: $checkedConvert('id', (v) => v as String?),
content: $checkedConvert('content', (v) => v as String?),
tokenUsage: $checkedConvert(
'tokenUsage',
(v) => v == null
? null
: LLMTokenUsage.fromJson(v as Map<String, dynamic>)),
finishReason: $checkedConvert('finishReason', (v) => v as String?),
toolCallResults: $checkedConvert(
'toolCallResults',
(v) => (v as List<dynamic>?)
?.map((e) =>
LLMToolCallResult.fromJson(e as Map<String, dynamic>))
.toList()),
metadata:
$checkedConvert('metadata', (v) => v as Map<String, dynamic>?),
streamChunks: $checkedConvert('streamChunks',
(v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
);
return val;
},
);
Map<String, dynamic> _$LLMResponseToJson(LLMResponse instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('id', instance.id);
writeNotNull('content', instance.content);
writeNotNull('tokenUsage', instance.tokenUsage?.toJson());
writeNotNull('finishReason', instance.finishReason);
writeNotNull('toolCallResults',
instance.toolCallResults?.map((e) => e.toJson()).toList());
writeNotNull('metadata', instance.metadata);
writeNotNull('streamChunks', instance.streamChunks);
return val;
}
LLMMessage _$LLMMessageFromJson(Map<String, dynamic> json) => $checkedCreate(
'LLMMessage',
json,
($checkedConvert) {
final val = LLMMessage(
role: $checkedConvert('role', (v) => v as String),
content: $checkedConvert('content', (v) => v as String?),
name: $checkedConvert('name', (v) => v as String?),
metadata:
$checkedConvert('metadata', (v) => v as Map<String, dynamic>?),
);
return val;
},
);
Map<String, dynamic> _$LLMMessageToJson(LLMMessage instance) {
final val = <String, dynamic>{
'role': instance.role,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('content', instance.content);
writeNotNull('name', instance.name);
writeNotNull('metadata', instance.metadata);
return val;
}
LLMTool _$LLMToolFromJson(Map<String, dynamic> json) => $checkedCreate(
'LLMTool',
json,
($checkedConvert) {
final val = LLMTool(
name: $checkedConvert('name', (v) => v as String),
description: $checkedConvert('description', (v) => v as String?),
parameters:
$checkedConvert('parameters', (v) => v as Map<String, dynamic>?),
);
return val;
},
);
Map<String, dynamic> _$LLMToolToJson(LLMTool instance) {
final val = <String, dynamic>{
'name': instance.name,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('description', instance.description);
writeNotNull('parameters', instance.parameters);
return val;
}
LLMToolCall _$LLMToolCallFromJson(Map<String, dynamic> json) => $checkedCreate(
'LLMToolCall',
json,
($checkedConvert) {
final val = LLMToolCall(
id: $checkedConvert('id', (v) => v as String),
name: $checkedConvert('name', (v) => v as String),
arguments:
$checkedConvert('arguments', (v) => v as Map<String, dynamic>?),
timestamp: $checkedConvert('timestamp',
(v) => v == null ? null : DateTime.parse(v as String)),
);
return val;
},
);
Map<String, dynamic> _$LLMToolCallToJson(LLMToolCall instance) {
final val = <String, dynamic>{
'id': instance.id,
'name': instance.name,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('arguments', instance.arguments);
writeNotNull('timestamp', instance.timestamp?.toIso8601String());
return val;
}
LLMToolCallResult _$LLMToolCallResultFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'LLMToolCallResult',
json,
($checkedConvert) {
final val = LLMToolCallResult(
toolCallId: $checkedConvert('toolCallId', (v) => v as String),
result: $checkedConvert('result', (v) => v as String?),
error: $checkedConvert(
'error',
(v) => v == null
? null
: LLMError.fromJson(v as Map<String, dynamic>)),
);
return val;
},
);
Map<String, dynamic> _$LLMToolCallResultToJson(LLMToolCallResult instance) {
final val = <String, dynamic>{
'toolCallId': instance.toolCallId,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('result', instance.result);
writeNotNull('error', instance.error?.toJson());
return val;
}
LLMTokenUsage _$LLMTokenUsageFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'LLMTokenUsage',
json,
($checkedConvert) {
final val = LLMTokenUsage(
promptTokens:
$checkedConvert('promptTokens', (v) => (v as num?)?.toInt()),
completionTokens:
$checkedConvert('completionTokens', (v) => (v as num?)?.toInt()),
totalTokens:
$checkedConvert('totalTokens', (v) => (v as num?)?.toInt()),
inputTokens:
$checkedConvert('inputTokens', (v) => (v as num?)?.toInt()),
outputTokens:
$checkedConvert('outputTokens', (v) => (v as num?)?.toInt()),
reasoningTokens:
$checkedConvert('reasoningTokens', (v) => (v as num?)?.toInt()),
cachedTokens:
$checkedConvert('cachedTokens', (v) => (v as num?)?.toInt()),
);
return val;
},
);
Map<String, dynamic> _$LLMTokenUsageToJson(LLMTokenUsage instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('promptTokens', instance.promptTokens);
writeNotNull('completionTokens', instance.completionTokens);
writeNotNull('totalTokens', instance.totalTokens);
writeNotNull('inputTokens', instance.inputTokens);
writeNotNull('outputTokens', instance.outputTokens);
writeNotNull('reasoningTokens', instance.reasoningTokens);
writeNotNull('cachedTokens', instance.cachedTokens);
return val;
}
LLMPerformanceMetrics _$LLMPerformanceMetricsFromJson(
Map<String, dynamic> json) =>
$checkedCreate(
'LLMPerformanceMetrics',
json,
($checkedConvert) {
final val = LLMPerformanceMetrics(
requestLatencyMs:
$checkedConvert('requestLatencyMs', (v) => (v as num?)?.toInt()),
firstTokenLatencyMs: $checkedConvert(
'firstTokenLatencyMs', (v) => (v as num?)?.toInt()),
totalDurationMs:
$checkedConvert('totalDurationMs', (v) => (v as num?)?.toInt()),
tokensPerSecond: $checkedConvert(
'tokensPerSecond', (v) => (v as num?)?.toDouble()),
charactersPerSecond: $checkedConvert(
'charactersPerSecond', (v) => (v as num?)?.toDouble()),
queueTimeMs:
$checkedConvert('queueTimeMs', (v) => (v as num?)?.toInt()),
processingTimeMs:
$checkedConvert('processingTimeMs', (v) => (v as num?)?.toInt()),
);
return val;
},
);
Map<String, dynamic> _$LLMPerformanceMetricsToJson(
LLMPerformanceMetrics instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('requestLatencyMs', instance.requestLatencyMs);
writeNotNull('firstTokenLatencyMs', instance.firstTokenLatencyMs);
writeNotNull('totalDurationMs', instance.totalDurationMs);
writeNotNull('tokensPerSecond', instance.tokensPerSecond);
writeNotNull('charactersPerSecond', instance.charactersPerSecond);
writeNotNull('queueTimeMs', instance.queueTimeMs);
writeNotNull('processingTimeMs', instance.processingTimeMs);
return val;
}
LLMError _$LLMErrorFromJson(Map<String, dynamic> json) => $checkedCreate(
'LLMError',
json,
($checkedConvert) {
final val = LLMError(
type: $checkedConvert('type', (v) => v as String?),
message: $checkedConvert('message', (v) => v as String?),
code: $checkedConvert('code', (v) => v as String?),
stackTrace: $checkedConvert('stackTrace', (v) => v as String?),
details:
$checkedConvert('details', (v) => v as Map<String, dynamic>?),
);
return val;
},
);
Map<String, dynamic> _$LLMErrorToJson(LLMError instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('type', instance.type);
writeNotNull('message', instance.message);
writeNotNull('code', instance.code);
writeNotNull('stackTrace', instance.stackTrace);
writeNotNull('details', instance.details);
return val;
}
LLMStatistics _$LLMStatisticsFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'LLMStatistics',
json,
($checkedConvert) {
final val = LLMStatistics(
totalCalls: $checkedConvert('totalCalls', (v) => (v as num).toInt()),
successfulCalls:
$checkedConvert('successfulCalls', (v) => (v as num).toInt()),
failedCalls:
$checkedConvert('failedCalls', (v) => (v as num).toInt()),
successRate:
$checkedConvert('successRate', (v) => (v as num).toDouble()),
averageLatency:
$checkedConvert('averageLatency', (v) => (v as num).toDouble()),
totalTokens:
$checkedConvert('totalTokens', (v) => (v as num).toInt()),
startTime: $checkedConvert('startTime',
(v) => v == null ? null : DateTime.parse(v as String)),
endTime: $checkedConvert(
'endTime', (v) => v == null ? null : DateTime.parse(v as String)),
details:
$checkedConvert('details', (v) => v as Map<String, dynamic>?),
);
return val;
},
);
Map<String, dynamic> _$LLMStatisticsToJson(LLMStatistics instance) {
final val = <String, dynamic>{
'totalCalls': instance.totalCalls,
'successfulCalls': instance.successfulCalls,
'failedCalls': instance.failedCalls,
'successRate': instance.successRate,
'averageLatency': instance.averageLatency,
'totalTokens': instance.totalTokens,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('startTime', instance.startTime?.toIso8601String());
writeNotNull('endTime', instance.endTime?.toIso8601String());
writeNotNull('details', instance.details);
return val;
}
ProviderStatistics _$ProviderStatisticsFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'ProviderStatistics',
json,
($checkedConvert) {
final val = ProviderStatistics(
provider: $checkedConvert('provider', (v) => v as String),
statistics: $checkedConvert('statistics',
(v) => LLMStatistics.fromJson(v as Map<String, dynamic>)),
models: $checkedConvert(
'models',
(v) => (v as List<dynamic>)
.map((e) =>
ModelStatistics.fromJson(e as Map<String, dynamic>))
.toList()),
);
return val;
},
);
Map<String, dynamic> _$ProviderStatisticsToJson(ProviderStatistics instance) =>
<String, dynamic>{
'provider': instance.provider,
'statistics': instance.statistics.toJson(),
'models': instance.models.map((e) => e.toJson()).toList(),
};
ModelStatistics _$ModelStatisticsFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'ModelStatistics',
json,
($checkedConvert) {
final val = ModelStatistics(
modelName: $checkedConvert('modelName', (v) => v as String),
provider: $checkedConvert('provider', (v) => v as String),
statistics: $checkedConvert('statistics',
(v) => LLMStatistics.fromJson(v as Map<String, dynamic>)),
);
return val;
},
);
Map<String, dynamic> _$ModelStatisticsToJson(ModelStatistics instance) =>
<String, dynamic>{
'modelName': instance.modelName,
'provider': instance.provider,
'statistics': instance.statistics.toJson(),
};
UserStatistics _$UserStatisticsFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'UserStatistics',
json,
($checkedConvert) {
final val = UserStatistics(
userId: $checkedConvert('userId', (v) => v as String),
username: $checkedConvert('username', (v) => v as String?),
statistics: $checkedConvert('statistics',
(v) => LLMStatistics.fromJson(v as Map<String, dynamic>)),
topModels: $checkedConvert('topModels',
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
topProviders: $checkedConvert('topProviders',
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
);
return val;
},
);
Map<String, dynamic> _$UserStatisticsToJson(UserStatistics instance) {
final val = <String, dynamic>{
'userId': instance.userId,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('username', instance.username);
val['statistics'] = instance.statistics.toJson();
val['topModels'] = instance.topModels;
val['topProviders'] = instance.topProviders;
return val;
}
ErrorStatistics _$ErrorStatisticsFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'ErrorStatistics',
json,
($checkedConvert) {
final val = ErrorStatistics(
errorType: $checkedConvert('errorType', (v) => v as String),
count: $checkedConvert('count', (v) => (v as num).toInt()),
percentage:
$checkedConvert('percentage', (v) => (v as num).toDouble()),
topErrorMessages: $checkedConvert('topErrorMessages',
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
affectedModels: $checkedConvert('affectedModels',
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
);
return val;
},
);
Map<String, dynamic> _$ErrorStatisticsToJson(ErrorStatistics instance) =>
<String, dynamic>{
'errorType': instance.errorType,
'count': instance.count,
'percentage': instance.percentage,
'topErrorMessages': instance.topErrorMessages,
'affectedModels': instance.affectedModels,
};
PerformanceStatistics _$PerformanceStatisticsFromJson(
Map<String, dynamic> json) =>
$checkedCreate(
'PerformanceStatistics',
json,
($checkedConvert) {
final val = PerformanceStatistics(
averageLatency:
$checkedConvert('averageLatency', (v) => (v as num).toDouble()),
medianLatency:
$checkedConvert('medianLatency', (v) => (v as num).toDouble()),
p95Latency:
$checkedConvert('p95Latency', (v) => (v as num).toDouble()),
p99Latency:
$checkedConvert('p99Latency', (v) => (v as num).toDouble()),
averageThroughput: $checkedConvert(
'averageThroughput', (v) => (v as num).toDouble()),
latencyTrends: $checkedConvert(
'latencyTrends',
(v) => (v as List<dynamic>)
.map((e) =>
TimeBasedMetric.fromJson(e as Map<String, dynamic>))
.toList()),
throughputTrends: $checkedConvert(
'throughputTrends',
(v) => (v as List<dynamic>)
.map((e) =>
TimeBasedMetric.fromJson(e as Map<String, dynamic>))
.toList()),
);
return val;
},
);
Map<String, dynamic> _$PerformanceStatisticsToJson(
PerformanceStatistics instance) =>
<String, dynamic>{
'averageLatency': instance.averageLatency,
'medianLatency': instance.medianLatency,
'p95Latency': instance.p95Latency,
'p99Latency': instance.p99Latency,
'averageThroughput': instance.averageThroughput,
'latencyTrends': instance.latencyTrends.map((e) => e.toJson()).toList(),
'throughputTrends':
instance.throughputTrends.map((e) => e.toJson()).toList(),
};
TimeBasedMetric _$TimeBasedMetricFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'TimeBasedMetric',
json,
($checkedConvert) {
final val = TimeBasedMetric(
timestamp:
$checkedConvert('timestamp', (v) => DateTime.parse(v as String)),
value: $checkedConvert('value', (v) => (v as num).toDouble()),
label: $checkedConvert('label', (v) => v as String?),
);
return val;
},
);
Map<String, dynamic> _$TimeBasedMetricToJson(TimeBasedMetric instance) {
final val = <String, dynamic>{
'timestamp': instance.timestamp.toIso8601String(),
'value': instance.value,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('label', instance.label);
return val;
}
SystemHealthStatus _$SystemHealthStatusFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'SystemHealthStatus',
json,
($checkedConvert) {
final val = SystemHealthStatus(
status: $checkedConvert(
'status',
(v) =>
$enumDecodeNullable(_$HealthStatusEnumMap, v) ??
HealthStatus.healthy),
components: $checkedConvert(
'components',
(v) => (v as Map<String, dynamic>).map(
(k, e) => MapEntry(
k, ComponentHealth.fromJson(e as Map<String, dynamic>)),
)),
message: $checkedConvert('message', (v) => v as String?),
lastChecked: $checkedConvert('lastChecked',
(v) => v == null ? null : DateTime.parse(v as String)),
);
return val;
},
);
Map<String, dynamic> _$SystemHealthStatusToJson(SystemHealthStatus instance) {
final val = <String, dynamic>{
'status': _$HealthStatusEnumMap[instance.status]!,
'components': instance.components.map((k, e) => MapEntry(k, e.toJson())),
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('message', instance.message);
writeNotNull('lastChecked', instance.lastChecked?.toIso8601String());
return val;
}
const _$HealthStatusEnumMap = {
HealthStatus.healthy: 'healthy',
HealthStatus.degraded: 'degraded',
HealthStatus.unhealthy: 'unhealthy',
HealthStatus.unknown: 'unknown',
};
ComponentHealth _$ComponentHealthFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'ComponentHealth',
json,
($checkedConvert) {
final val = ComponentHealth(
status: $checkedConvert(
'status',
(v) =>
$enumDecodeNullable(_$HealthStatusEnumMap, v) ??
HealthStatus.healthy),
message: $checkedConvert('message', (v) => v as String?),
metrics:
$checkedConvert('metrics', (v) => v as Map<String, dynamic>?),
);
return val;
},
);
Map<String, dynamic> _$ComponentHealthToJson(ComponentHealth instance) {
final val = <String, dynamic>{
'status': _$HealthStatusEnumMap[instance.status]!,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('message', instance.message);
writeNotNull('metrics', instance.metrics);
return val;
}
LLMTraceSearchCriteria _$LLMTraceSearchCriteriaFromJson(
Map<String, dynamic> json) =>
$checkedCreate(
'LLMTraceSearchCriteria',
json,
($checkedConvert) {
final val = LLMTraceSearchCriteria(
userId: $checkedConvert('userId', (v) => v as String?),
provider: $checkedConvert('provider', (v) => v as String?),
model: $checkedConvert('model', (v) => v as String?),
sessionId: $checkedConvert('sessionId', (v) => v as String?),
hasError: $checkedConvert('hasError', (v) => v as bool?),
status: $checkedConvert(
'status', (v) => $enumDecodeNullable(_$LLMTraceStatusEnumMap, v)),
startTime: $checkedConvert('startTime',
(v) => v == null ? null : DateTime.parse(v as String)),
endTime: $checkedConvert(
'endTime', (v) => v == null ? null : DateTime.parse(v as String)),
page: $checkedConvert('page', (v) => (v as num?)?.toInt() ?? 0),
size: $checkedConvert('size', (v) => (v as num?)?.toInt() ?? 20),
sortBy: $checkedConvert('sortBy', (v) => v as String? ?? 'timestamp'),
sortDir: $checkedConvert('sortDir', (v) => v as String? ?? 'desc'),
);
return val;
},
);
Map<String, dynamic> _$LLMTraceSearchCriteriaToJson(
LLMTraceSearchCriteria instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('userId', instance.userId);
writeNotNull('provider', instance.provider);
writeNotNull('model', instance.model);
writeNotNull('sessionId', instance.sessionId);
writeNotNull('hasError', instance.hasError);
writeNotNull('status', _$LLMTraceStatusEnumMap[instance.status]);
writeNotNull('startTime', instance.startTime?.toIso8601String());
writeNotNull('endTime', instance.endTime?.toIso8601String());
val['page'] = instance.page;
val['size'] = instance.size;
val['sortBy'] = instance.sortBy;
val['sortDir'] = instance.sortDir;
return val;
}
ApiResponse<T> _$ApiResponseFromJson<T>(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) =>
$checkedCreate(
'ApiResponse',
json,
($checkedConvert) {
final val = ApiResponse<T>(
success: $checkedConvert('success', (v) => v as bool),
message: $checkedConvert('message', (v) => v as String?),
data: $checkedConvert(
'data', (v) => _$nullableGenericFromJson(v, fromJsonT)),
error: $checkedConvert('error', (v) => v as String?),
);
return val;
},
);
Map<String, dynamic> _$ApiResponseToJson<T>(
ApiResponse<T> instance,
Object? Function(T value) toJsonT,
) {
final val = <String, dynamic>{
'success': instance.success,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('message', instance.message);
writeNotNull('data', _$nullableGenericToJson(instance.data, toJsonT));
writeNotNull('error', instance.error);
return val;
}
T? _$nullableGenericFromJson<T>(
Object? input,
T Function(Object? json) fromJson,
) =>
input == null ? null : fromJson(input);
Object? _$nullableGenericToJson<T>(
T? input,
Object? Function(T value) toJson,
) =>
input == null ? null : toJson(input);
PagedResponse<T> _$PagedResponseFromJson<T>(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) =>
$checkedCreate(
'PagedResponse',
json,
($checkedConvert) {
final val = PagedResponse<T>(
content: $checkedConvert(
'content', (v) => (v as List<dynamic>).map(fromJsonT).toList()),
page: $checkedConvert('page', (v) => (v as num).toInt()),
size: $checkedConvert('size', (v) => (v as num).toInt()),
totalElements:
$checkedConvert('totalElements', (v) => (v as num).toInt()),
totalPages: $checkedConvert('totalPages', (v) => (v as num).toInt()),
first: $checkedConvert('first', (v) => v as bool? ?? false),
last: $checkedConvert('last', (v) => v as bool? ?? false),
);
return val;
},
);
Map<String, dynamic> _$PagedResponseToJson<T>(
PagedResponse<T> instance,
Object? Function(T value) toJsonT,
) =>
<String, dynamic>{
'content': instance.content.map(toJsonT).toList(),
'page': instance.page,
'size': instance.size,
'totalElements': instance.totalElements,
'totalPages': instance.totalPages,
'first': instance.first,
'last': instance.last,
};
CursorPageResponse<T> _$CursorPageResponseFromJson<T>(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) =>
$checkedCreate(
'CursorPageResponse',
json,
($checkedConvert) {
final val = CursorPageResponse<T>(
items: $checkedConvert(
'items', (v) => (v as List<dynamic>).map(fromJsonT).toList()),
nextCursor: $checkedConvert('nextCursor', (v) => v as String?),
hasMore: $checkedConvert('hasMore', (v) => v as bool? ?? false),
);
return val;
},
);
Map<String, dynamic> _$CursorPageResponseToJson<T>(
CursorPageResponse<T> instance,
Object? Function(T value) toJsonT,
) {
final val = <String, dynamic>{
'items': instance.items.map(toJsonT).toList(),
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('nextCursor', instance.nextCursor);
val['hasMore'] = instance.hasMore;
return val;
}

View File

@@ -0,0 +1,429 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import '../../utils/date_time_parser.dart';
part 'subscription_models.g.dart';
/// 订阅计划模型
@JsonSerializable()
class SubscriptionPlan extends Equatable {
final String? id;
final String planName;
final String? description;
final double price;
final String currency;
final BillingCycle billingCycle;
final String? roleId;
final int? creditsGranted;
final bool active;
final bool recommended;
final int priority;
final Map<String, dynamic>? features;
final int trialDays;
final int maxUsers;
final DateTime? createdAt;
final DateTime? updatedAt;
const SubscriptionPlan({
this.id,
required this.planName,
this.description,
required this.price,
required this.currency,
required this.billingCycle,
this.roleId,
this.creditsGranted,
this.active = true,
this.recommended = false,
this.priority = 0,
this.features,
this.trialDays = 0,
this.maxUsers = -1,
this.createdAt,
this.updatedAt,
});
factory SubscriptionPlan.fromJson(Map<String, dynamic> json) {
// 兼容后端字段可能为空或类型不一致(如 BigDecimal 序列化为字符串)
final dynamic priceRaw = json['price'];
final double parsedPrice = priceRaw is num
? priceRaw.toDouble()
: (priceRaw is String ? double.tryParse(priceRaw) ?? 0.0 : 0.0);
final dynamic priorityRaw = json['priority'];
final int parsedPriority = priorityRaw is num
? priorityRaw.toInt()
: (priorityRaw is String ? int.tryParse(priorityRaw) ?? 0 : 0);
final dynamic creditsRaw = json['creditsGranted'];
final int? parsedCredits = creditsRaw == null
? null
: (creditsRaw is num
? creditsRaw.toInt()
: (creditsRaw is String ? int.tryParse(creditsRaw) : null));
final dynamic activeRaw = json['active'];
final bool parsedActive = activeRaw is bool
? activeRaw
: (activeRaw is String ? activeRaw.toLowerCase() == 'true' : true);
final dynamic recommendedRaw = json['recommended'];
final bool parsedRecommended = recommendedRaw is bool
? recommendedRaw
: (recommendedRaw is String ? recommendedRaw.toLowerCase() == 'true' : false);
final featuresRaw = json['features'];
final Map<String, dynamic>? parsedFeatures =
featuresRaw is Map<String, dynamic> ? featuresRaw : null;
return SubscriptionPlan(
id: json['id'] as String?,
planName: (json['planName'] as String?) ?? '未命名套餐',
description: json['description'] as String?,
price: parsedPrice,
currency: (json['currency'] as String?) ?? 'CNY',
billingCycle: _parseBillingCycle(json['billingCycle']),
roleId: json['roleId'] as String?,
creditsGranted: parsedCredits,
active: parsedActive,
recommended: parsedRecommended,
priority: parsedPriority,
features: parsedFeatures,
trialDays: ((json['trialDays'] is String)
? int.tryParse(json['trialDays'])
: (json['trialDays'] as num?))
?.toInt() ?? 0,
maxUsers: ((json['maxUsers'] is String)
? int.tryParse(json['maxUsers'])
: (json['maxUsers'] as num?))
?.toInt() ?? -1,
createdAt: json['createdAt'] != null ? parseBackendDateTime(json['createdAt']) : null,
updatedAt: json['updatedAt'] != null ? parseBackendDateTime(json['updatedAt']) : null,
);
}
Map<String, dynamic> toJson() => _$SubscriptionPlanToJson(this);
@override
List<Object?> get props => [
id,
planName,
description,
price,
currency,
billingCycle,
roleId,
creditsGranted,
active,
recommended,
priority,
features,
trialDays,
maxUsers,
createdAt,
updatedAt,
];
/// 获取月度等价价格
double get monthlyEquivalentPrice {
switch (billingCycle) {
case BillingCycle.monthly:
return price;
case BillingCycle.quarterly:
return price / 3;
case BillingCycle.yearly:
return price / 12;
case BillingCycle.lifetime:
return price / 120; // 假设10年使用期
}
}
/// 获取计费周期显示文本
String get billingCycleText {
switch (billingCycle) {
case BillingCycle.monthly:
return '月付';
case BillingCycle.quarterly:
return '季付';
case BillingCycle.yearly:
return '年付';
case BillingCycle.lifetime:
return '终身';
}
}
/// 获取格式化价格
String get formattedPrice {
return '$currency ${price.toStringAsFixed(2)}';
}
/// 解析BillingCycle枚举
static BillingCycle _parseBillingCycle(dynamic value) {
if (value == null) return BillingCycle.monthly;
final stringValue = value.toString().toUpperCase();
switch (stringValue) {
case 'MONTHLY':
return BillingCycle.monthly;
case 'QUARTERLY':
return BillingCycle.quarterly;
case 'YEARLY':
return BillingCycle.yearly;
case 'LIFETIME':
return BillingCycle.lifetime;
default:
return BillingCycle.monthly;
}
}
/// 创建副本
SubscriptionPlan copyWith({
String? id,
String? planName,
String? description,
double? price,
String? currency,
BillingCycle? billingCycle,
String? roleId,
int? creditsGranted,
bool? active,
bool? recommended,
int? priority,
Map<String, dynamic>? features,
int? trialDays,
int? maxUsers,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return SubscriptionPlan(
id: id ?? this.id,
planName: planName ?? this.planName,
description: description ?? this.description,
price: price ?? this.price,
currency: currency ?? this.currency,
billingCycle: billingCycle ?? this.billingCycle,
roleId: roleId ?? this.roleId,
creditsGranted: creditsGranted ?? this.creditsGranted,
active: active ?? this.active,
recommended: recommended ?? this.recommended,
priority: priority ?? this.priority,
features: features ?? this.features,
trialDays: trialDays ?? this.trialDays,
maxUsers: maxUsers ?? this.maxUsers,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}
/// 计费周期枚举
enum BillingCycle {
@JsonValue('MONTHLY')
monthly,
@JsonValue('QUARTERLY')
quarterly,
@JsonValue('YEARLY')
yearly,
@JsonValue('LIFETIME')
lifetime,
}
/// 用户订阅模型
@JsonSerializable()
class UserSubscription extends Equatable {
final String? id;
final String userId;
final String planId;
final DateTime? startDate;
final DateTime? endDate;
final SubscriptionStatus status;
final bool autoRenewal;
final String? paymentMethod;
final String? transactionId;
final int creditsUsed;
final int totalCredits;
final DateTime? canceledAt;
final String? cancelReason;
final DateTime? trialEndDate;
final bool isTrial;
final DateTime? createdAt;
final DateTime? updatedAt;
const UserSubscription({
this.id,
required this.userId,
required this.planId,
this.startDate,
this.endDate,
required this.status,
this.autoRenewal = false,
this.paymentMethod,
this.transactionId,
this.creditsUsed = 0,
this.totalCredits = 0,
this.canceledAt,
this.cancelReason,
this.trialEndDate,
this.isTrial = false,
this.createdAt,
this.updatedAt,
});
factory UserSubscription.fromJson(Map<String, dynamic> json) {
return UserSubscription(
id: json['id'] as String?,
userId: json['userId'] as String,
planId: json['planId'] as String,
startDate: json['startDate'] != null ? parseBackendDateTime(json['startDate']) : null,
endDate: json['endDate'] != null ? parseBackendDateTime(json['endDate']) : null,
status: _parseSubscriptionStatus(json['status']),
autoRenewal: json['autoRenewal'] as bool? ?? false,
paymentMethod: json['paymentMethod'] as String?,
transactionId: json['transactionId'] as String?,
creditsUsed: (json['creditsUsed'] as num?)?.toInt() ?? 0,
totalCredits: (json['totalCredits'] as num?)?.toInt() ?? 0,
canceledAt: json['canceledAt'] != null ? parseBackendDateTime(json['canceledAt']) : null,
cancelReason: json['cancelReason'] as String?,
trialEndDate: json['trialEndDate'] != null ? parseBackendDateTime(json['trialEndDate']) : null,
isTrial: json['isTrial'] as bool? ?? false,
createdAt: json['createdAt'] != null ? parseBackendDateTime(json['createdAt']) : null,
updatedAt: json['updatedAt'] != null ? parseBackendDateTime(json['updatedAt']) : null,
);
}
Map<String, dynamic> toJson() => _$UserSubscriptionToJson(this);
@override
List<Object?> get props => [
id,
userId,
planId,
startDate,
endDate,
status,
autoRenewal,
paymentMethod,
transactionId,
creditsUsed,
totalCredits,
canceledAt,
cancelReason,
trialEndDate,
isTrial,
createdAt,
updatedAt,
];
/// 获取剩余积分
int get remainingCredits => (totalCredits - creditsUsed).clamp(0, totalCredits);
/// 检查订阅是否有效
bool get isValid {
final now = DateTime.now();
return (status == SubscriptionStatus.active || status == SubscriptionStatus.trial) &&
(endDate == null || endDate!.isAfter(now));
}
/// 检查是否即将过期7天内
bool get isExpiringSoon {
if (endDate == null) return false;
final now = DateTime.now();
final sevenDaysLater = now.add(const Duration(days: 7));
return endDate!.isBefore(sevenDaysLater) && endDate!.isAfter(now);
}
/// 解析SubscriptionStatus枚举
static SubscriptionStatus _parseSubscriptionStatus(dynamic value) {
if (value == null) return SubscriptionStatus.active;
final stringValue = value.toString().toUpperCase();
switch (stringValue) {
case 'ACTIVE':
return SubscriptionStatus.active;
case 'TRIAL':
return SubscriptionStatus.trial;
case 'CANCELED':
return SubscriptionStatus.canceled;
case 'EXPIRED':
return SubscriptionStatus.expired;
case 'SUSPENDED':
return SubscriptionStatus.suspended;
case 'REFUNDED':
return SubscriptionStatus.refunded;
default:
return SubscriptionStatus.active;
}
}
/// 获取状态显示文本
String get statusText {
switch (status) {
case SubscriptionStatus.active:
return '活跃';
case SubscriptionStatus.trial:
return '试用期';
case SubscriptionStatus.canceled:
return '已取消';
case SubscriptionStatus.expired:
return '已过期';
case SubscriptionStatus.suspended:
return '暂停';
case SubscriptionStatus.refunded:
return '已退款';
}
}
}
/// 订阅状态枚举
enum SubscriptionStatus {
@JsonValue('ACTIVE')
active,
@JsonValue('TRIAL')
trial,
@JsonValue('CANCELED')
canceled,
@JsonValue('EXPIRED')
expired,
@JsonValue('SUSPENDED')
suspended,
@JsonValue('REFUNDED')
refunded,
}
/// 订阅统计信息
@JsonSerializable()
class SubscriptionStatistics extends Equatable {
final int totalPlans;
final int activePlans;
final int totalSubscriptions;
final int activeSubscriptions;
final int trialSubscriptions;
final double monthlyRevenue;
final double yearlyRevenue;
const SubscriptionStatistics({
required this.totalPlans,
required this.activePlans,
required this.totalSubscriptions,
required this.activeSubscriptions,
required this.trialSubscriptions,
required this.monthlyRevenue,
required this.yearlyRevenue,
});
factory SubscriptionStatistics.fromJson(Map<String, dynamic> json) =>
_$SubscriptionStatisticsFromJson(json);
Map<String, dynamic> toJson() => _$SubscriptionStatisticsToJson(this);
@override
List<Object?> get props => [
totalPlans,
activePlans,
totalSubscriptions,
activeSubscriptions,
trialSubscriptions,
monthlyRevenue,
yearlyRevenue,
];
}

View File

@@ -0,0 +1,191 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'subscription_models.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SubscriptionPlan _$SubscriptionPlanFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'SubscriptionPlan',
json,
($checkedConvert) {
final val = SubscriptionPlan(
id: $checkedConvert('id', (v) => v as String?),
planName: $checkedConvert('planName', (v) => v as String),
description: $checkedConvert('description', (v) => v as String?),
price: $checkedConvert('price', (v) => (v as num).toDouble()),
currency: $checkedConvert('currency', (v) => v as String),
billingCycle: $checkedConvert(
'billingCycle', (v) => $enumDecode(_$BillingCycleEnumMap, v)),
roleId: $checkedConvert('roleId', (v) => v as String?),
creditsGranted:
$checkedConvert('creditsGranted', (v) => (v as num?)?.toInt()),
active: $checkedConvert('active', (v) => v as bool? ?? true),
recommended:
$checkedConvert('recommended', (v) => v as bool? ?? false),
priority:
$checkedConvert('priority', (v) => (v as num?)?.toInt() ?? 0),
features:
$checkedConvert('features', (v) => v as Map<String, dynamic>?),
trialDays:
$checkedConvert('trialDays', (v) => (v as num?)?.toInt() ?? 0),
maxUsers:
$checkedConvert('maxUsers', (v) => (v as num?)?.toInt() ?? -1),
createdAt: $checkedConvert('createdAt',
(v) => v == null ? null : DateTime.parse(v as String)),
updatedAt: $checkedConvert('updatedAt',
(v) => v == null ? null : DateTime.parse(v as String)),
);
return val;
},
);
Map<String, dynamic> _$SubscriptionPlanToJson(SubscriptionPlan instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('id', instance.id);
val['planName'] = instance.planName;
writeNotNull('description', instance.description);
val['price'] = instance.price;
val['currency'] = instance.currency;
val['billingCycle'] = _$BillingCycleEnumMap[instance.billingCycle]!;
writeNotNull('roleId', instance.roleId);
writeNotNull('creditsGranted', instance.creditsGranted);
val['active'] = instance.active;
val['recommended'] = instance.recommended;
val['priority'] = instance.priority;
writeNotNull('features', instance.features);
val['trialDays'] = instance.trialDays;
val['maxUsers'] = instance.maxUsers;
writeNotNull('createdAt', instance.createdAt?.toIso8601String());
writeNotNull('updatedAt', instance.updatedAt?.toIso8601String());
return val;
}
const _$BillingCycleEnumMap = {
BillingCycle.monthly: 'MONTHLY',
BillingCycle.quarterly: 'QUARTERLY',
BillingCycle.yearly: 'YEARLY',
BillingCycle.lifetime: 'LIFETIME',
};
UserSubscription _$UserSubscriptionFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'UserSubscription',
json,
($checkedConvert) {
final val = UserSubscription(
id: $checkedConvert('id', (v) => v as String?),
userId: $checkedConvert('userId', (v) => v as String),
planId: $checkedConvert('planId', (v) => v as String),
startDate: $checkedConvert('startDate',
(v) => v == null ? null : DateTime.parse(v as String)),
endDate: $checkedConvert(
'endDate', (v) => v == null ? null : DateTime.parse(v as String)),
status: $checkedConvert(
'status', (v) => $enumDecode(_$SubscriptionStatusEnumMap, v)),
autoRenewal:
$checkedConvert('autoRenewal', (v) => v as bool? ?? false),
paymentMethod: $checkedConvert('paymentMethod', (v) => v as String?),
transactionId: $checkedConvert('transactionId', (v) => v as String?),
creditsUsed:
$checkedConvert('creditsUsed', (v) => (v as num?)?.toInt() ?? 0),
totalCredits:
$checkedConvert('totalCredits', (v) => (v as num?)?.toInt() ?? 0),
canceledAt: $checkedConvert('canceledAt',
(v) => v == null ? null : DateTime.parse(v as String)),
cancelReason: $checkedConvert('cancelReason', (v) => v as String?),
trialEndDate: $checkedConvert('trialEndDate',
(v) => v == null ? null : DateTime.parse(v as String)),
isTrial: $checkedConvert('isTrial', (v) => v as bool? ?? false),
createdAt: $checkedConvert('createdAt',
(v) => v == null ? null : DateTime.parse(v as String)),
updatedAt: $checkedConvert('updatedAt',
(v) => v == null ? null : DateTime.parse(v as String)),
);
return val;
},
);
Map<String, dynamic> _$UserSubscriptionToJson(UserSubscription instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('id', instance.id);
val['userId'] = instance.userId;
val['planId'] = instance.planId;
writeNotNull('startDate', instance.startDate?.toIso8601String());
writeNotNull('endDate', instance.endDate?.toIso8601String());
val['status'] = _$SubscriptionStatusEnumMap[instance.status]!;
val['autoRenewal'] = instance.autoRenewal;
writeNotNull('paymentMethod', instance.paymentMethod);
writeNotNull('transactionId', instance.transactionId);
val['creditsUsed'] = instance.creditsUsed;
val['totalCredits'] = instance.totalCredits;
writeNotNull('canceledAt', instance.canceledAt?.toIso8601String());
writeNotNull('cancelReason', instance.cancelReason);
writeNotNull('trialEndDate', instance.trialEndDate?.toIso8601String());
val['isTrial'] = instance.isTrial;
writeNotNull('createdAt', instance.createdAt?.toIso8601String());
writeNotNull('updatedAt', instance.updatedAt?.toIso8601String());
return val;
}
const _$SubscriptionStatusEnumMap = {
SubscriptionStatus.active: 'ACTIVE',
SubscriptionStatus.trial: 'TRIAL',
SubscriptionStatus.canceled: 'CANCELED',
SubscriptionStatus.expired: 'EXPIRED',
SubscriptionStatus.suspended: 'SUSPENDED',
SubscriptionStatus.refunded: 'REFUNDED',
};
SubscriptionStatistics _$SubscriptionStatisticsFromJson(
Map<String, dynamic> json) =>
$checkedCreate(
'SubscriptionStatistics',
json,
($checkedConvert) {
final val = SubscriptionStatistics(
totalPlans: $checkedConvert('totalPlans', (v) => (v as num).toInt()),
activePlans:
$checkedConvert('activePlans', (v) => (v as num).toInt()),
totalSubscriptions:
$checkedConvert('totalSubscriptions', (v) => (v as num).toInt()),
activeSubscriptions:
$checkedConvert('activeSubscriptions', (v) => (v as num).toInt()),
trialSubscriptions:
$checkedConvert('trialSubscriptions', (v) => (v as num).toInt()),
monthlyRevenue:
$checkedConvert('monthlyRevenue', (v) => (v as num).toDouble()),
yearlyRevenue:
$checkedConvert('yearlyRevenue', (v) => (v as num).toDouble()),
);
return val;
},
);
Map<String, dynamic> _$SubscriptionStatisticsToJson(
SubscriptionStatistics instance) =>
<String, dynamic>{
'totalPlans': instance.totalPlans,
'activePlans': instance.activePlans,
'totalSubscriptions': instance.totalSubscriptions,
'activeSubscriptions': instance.activeSubscriptions,
'trialSubscriptions': instance.trialSubscriptions,
'monthlyRevenue': instance.monthlyRevenue,
'yearlyRevenue': instance.yearlyRevenue,
};

View File

@@ -0,0 +1,107 @@
/// AI上下文追踪选项枚举
enum AIContextTracking {
/// 总是包含在AI上下文中
/// 此条目被标记为全局其信息总是呈现给AI
always('always', '总是包含', '此条目被标记为全局其信息总是呈现给AI'),
/// 检测到时包含(默认)
/// 当在文本/选择/聊天消息中检测到此条目时,将其添加到上下文中
detected('detected', '检测到时包含', '当在文本/选择/聊天消息中检测到此条目时,将其添加到上下文中'),
/// 检测到时不包含
/// 即使检测到也不要将此条目添加到上下文中,但在被引用或手动添加为场景上下文时仍可拉入
dontInclude('dont_include', '检测到时不包含', '即使检测到也不要将此条目添加到上下文中,但在被引用或手动添加为场景上下文时仍可拉入'),
/// 从不包含
/// 此条目永远不会显示给AI对于私人笔记或无关信息很有用
never('never', '从不包含', '此条目永远不会显示给AI对于私人笔记或无关信息很有用');
const AIContextTracking(this.value, this.displayName, this.description);
final String value;
final String displayName;
final String description;
/// 根据值获取枚举
static AIContextTracking fromValue(String? value) {
if (value == null) return detected; // 默认值
return values.firstWhere(
(type) => type.value == value,
orElse: () => detected,
);
}
/// 获取所有追踪选项的显示名称
static List<String> get allDisplayNames {
return values.map((type) => type.displayName).toList();
}
/// 是否应该包含在AI上下文中
bool shouldIncludeInContext({
bool isDetected = false,
bool isManuallyAdded = false,
bool isReferenced = false,
}) {
switch (this) {
case always:
return true;
case detected:
return isDetected || isManuallyAdded || isReferenced;
case dontInclude:
return isManuallyAdded || isReferenced;
case never:
return false;
}
}
}
/// 设定引用修改选项枚举
enum SettingReferenceUpdate {
/// 修改此设定时,自动更新所有引用此设定的地方
update('update', '自动更新引用', '修改此设定时,自动更新所有引用此设定的地方'),
/// 修改此设定时,询问是否更新引用
ask('ask', '询问是否更新', '修改此设定时,询问是否更新引用'),
/// 修改此设定时,不更新引用
noUpdate('no_update', '不更新引用', '修改此设定时,不更新引用');
const SettingReferenceUpdate(this.value, this.displayName, this.description);
final String value;
final String displayName;
final String description;
/// 根据值获取枚举
static SettingReferenceUpdate fromValue(String? value) {
if (value == null) return ask; // 默认值
return values.firstWhere(
(type) => type.value == value,
orElse: () => ask,
);
}
}
/// 名称/别名追踪选项枚举
enum NameAliasTracking {
/// 通过名称/别名追踪此条目
track('track', '通过名称/别名追踪', '通过名称/别名追踪此条目'),
/// 不追踪此条目
noTrack('no_track', '不追踪', '不追踪此条目');
const NameAliasTracking(this.value, this.displayName, this.description);
final String value;
final String displayName;
final String description;
/// 根据值获取枚举
static NameAliasTracking fromValue(String? value) {
if (value == null) return track; // 默认值
return values.firstWhere(
(type) => type.value == value,
orElse: () => track,
);
}
}

View File

@@ -0,0 +1,408 @@
import 'package:ainoval/models/prompt_models.dart';
/// AI功能表单字段类型
enum AIFormFieldType {
instructions, // 指令字段
length, // 长度字段 (扩写/缩写)
style, // 重构方式字段 (重构)
contextSelection, // 上下文选择
smartContext, // 智能上下文开关
promptTemplate, // 提示词模板选择
temperature, // 温度滑动条
topP, // Top-P滑动条
memoryCutoff, // 记忆截断 (聊天)
quickAccess, // 快捷访问开关
}
/// 表单字段配置
class FormFieldConfig {
final AIFormFieldType type;
final String title;
final String description;
final bool isRequired;
final Map<String, dynamic>? options; // 用于存储字段特定选项
const FormFieldConfig({
required this.type,
required this.title,
required this.description,
this.isRequired = false,
this.options,
});
}
/// AI功能表单配置
class AIFeatureFormConfig {
static const Map<AIFeatureType, List<FormFieldConfig>> _configs = {
// 文本扩写
AIFeatureType.textExpansion: [
const FormFieldConfig(
type: AIFormFieldType.instructions,
title: '指令',
description: '应该如何扩写文本?',
options: {
'placeholder': 'e.g. 描述设定',
'presets': [
{'id': 'descriptive', 'title': '描述性扩写', 'content': '请为这段文本添加更详细的描述,包括环境、感官细节和人物心理描写。'},
{'id': 'dialogue', 'title': '对话扩写', 'content': '请为这段文本添加更多的对话和人物互动,展现人物性格。'},
{'id': 'action', 'title': '动作扩写', 'content': '请为这段文本添加更多的动作描写和情节发展。'},
],
},
),
const FormFieldConfig(
type: AIFormFieldType.length,
title: '长度',
description: '扩写后的文本应该多长?',
options: {
'radioOptions': [
{'value': 'double', 'label': '双倍'},
{'value': 'triple', 'label': '三倍'},
],
'placeholder': 'e.g. 400 words',
},
),
const FormFieldConfig(
type: AIFormFieldType.contextSelection,
title: '附加上下文',
description: '为AI提供的任何额外信息',
),
const FormFieldConfig(
type: AIFormFieldType.smartContext,
title: '智能上下文',
description: '使用AI自动检索相关背景信息提升生成质量',
),
const FormFieldConfig(
type: AIFormFieldType.promptTemplate,
title: '关联提示词模板',
description: '选择要关联的提示词模板(可选)',
),
const FormFieldConfig(
type: AIFormFieldType.temperature,
title: '温度',
description: '控制生成内容的创造性',
),
const FormFieldConfig(
type: AIFormFieldType.topP,
title: 'Top-P',
description: '控制生成内容的多样性',
),
const FormFieldConfig(
type: AIFormFieldType.quickAccess,
title: '快捷访问',
description: '是否在功能对话框中显示此预设',
),
],
// 文本缩写
AIFeatureType.textSummary: [
const FormFieldConfig(
type: AIFormFieldType.length,
title: '长度',
description: '缩短后的文本应该多长?',
isRequired: true,
options: {
'radioOptions': [
{'value': 'half', 'label': '一半'},
{'value': 'quarter', 'label': '四分之一'},
{'value': 'paragraph', 'label': '单段落'},
],
'placeholder': 'e.g. 100 words',
},
),
const FormFieldConfig(
type: AIFormFieldType.instructions,
title: '指令',
description: '为AI提供的任何可选额外指令和角色',
options: {
'placeholder': 'e.g. You are a...',
'presets': [
{'id': 'brief', 'title': '简洁摘要', 'content': '请将这段文本总结为简洁的要点。'},
{'id': 'detailed', 'title': '详细摘要', 'content': '请提供详细的摘要,保留关键细节。'},
],
},
),
const FormFieldConfig(
type: AIFormFieldType.contextSelection,
title: '附加上下文',
description: '为AI提供的任何额外信息',
),
const FormFieldConfig(
type: AIFormFieldType.smartContext,
title: '智能上下文',
description: '使用AI自动检索相关背景信息提升缩写质量',
),
const FormFieldConfig(
type: AIFormFieldType.promptTemplate,
title: '关联提示词模板',
description: '选择要关联的提示词模板(可选)',
),
const FormFieldConfig(
type: AIFormFieldType.temperature,
title: '温度',
description: '控制生成内容的创造性',
),
const FormFieldConfig(
type: AIFormFieldType.topP,
title: 'Top-P',
description: '控制生成内容的多样性',
),
const FormFieldConfig(
type: AIFormFieldType.quickAccess,
title: '快捷访问',
description: '是否在功能对话框中显示此预设',
),
],
// 文本重构
AIFeatureType.textRefactor: [
const FormFieldConfig(
type: AIFormFieldType.instructions,
title: '指令',
description: '应该如何重构文本?',
options: {
'placeholder': 'e.g. 重写以提高清晰度',
'presets': [
{'id': 'dramatic', 'title': '增强戏剧性', 'content': '让这段文字更具戏剧性和冲突感,增强情节张力。'},
{'id': 'style', 'title': '改变风格', 'content': '请将这段文字改写为更优雅/现代/古典的文学风格。'},
{'id': 'pov', 'title': '转换视角', 'content': '请将这段文字从第一人称改写为第三人称(或相反)。'},
{'id': 'mood', 'title': '调整情绪', 'content': '请调整这段文字的情绪氛围,使其更加轻松/严肃/神秘/温馨。'},
],
},
),
const FormFieldConfig(
type: AIFormFieldType.style,
title: '重构方式',
description: '重点关注哪个方面?',
options: {
'radioOptions': [
{'value': 'clarity', 'label': '清晰度'},
{'value': 'flow', 'label': '流畅性'},
{'value': 'tone', 'label': '语调'},
],
'placeholder': 'e.g. 更加正式',
},
),
const FormFieldConfig(
type: AIFormFieldType.contextSelection,
title: '附加上下文',
description: '为AI提供的任何额外信息',
),
const FormFieldConfig(
type: AIFormFieldType.smartContext,
title: '智能上下文',
description: '使用AI自动检索相关背景信息提升重构质量',
),
const FormFieldConfig(
type: AIFormFieldType.promptTemplate,
title: '关联提示词模板',
description: '选择要关联的提示词模板(可选)',
),
const FormFieldConfig(
type: AIFormFieldType.temperature,
title: '温度',
description: '控制生成内容的创造性',
),
const FormFieldConfig(
type: AIFormFieldType.topP,
title: 'Top-P',
description: '控制生成内容的多样性',
),
const FormFieldConfig(
type: AIFormFieldType.quickAccess,
title: '快捷访问',
description: '是否在功能对话框中显示此预设',
),
],
// AI聊天
AIFeatureType.aiChat: [
const FormFieldConfig(
type: AIFormFieldType.instructions,
title: 'Instructions',
description: 'Any (optional) additional instructions and roles for the AI',
options: {
'placeholder': 'e.g. You are a...',
},
),
const FormFieldConfig(
type: AIFormFieldType.contextSelection,
title: 'Additional Context',
description: 'Any additional information to provide to the AI',
),
const FormFieldConfig(
type: AIFormFieldType.smartContext,
title: 'Smart Context',
description: 'Use AI to automatically retrieve relevant background information',
),
const FormFieldConfig(
type: AIFormFieldType.promptTemplate,
title: '关联提示词模板',
description: '选择要关联的提示词模板(可选)',
),
const FormFieldConfig(
type: AIFormFieldType.temperature,
title: '温度',
description: '控制生成内容的创造性',
),
const FormFieldConfig(
type: AIFormFieldType.topP,
title: 'Top-P',
description: '控制生成内容的多样性',
),
const FormFieldConfig(
type: AIFormFieldType.memoryCutoff,
title: 'Memory Cutoff',
description: 'Specify a maximum number of message pairs to be sent to the AI. Any messages exceeding this limit will be ignored.',
options: {
'radioOptions': [
{'value': 14, 'label': '14 (Default)'},
{'value': 28, 'label': '28'},
{'value': 48, 'label': '48'},
{'value': 64, 'label': '64'},
],
'placeholder': 'e.g. 24',
},
),
const FormFieldConfig(
type: AIFormFieldType.quickAccess,
title: '快捷访问',
description: '是否在功能对话框中显示此预设',
),
],
// 🚀 新增:场景节拍生成
AIFeatureType.sceneBeatGeneration: [
const FormFieldConfig(
type: AIFormFieldType.instructions,
title: '指令',
description: '为AI提供的场景节拍生成指令',
options: {
'placeholder': 'e.g. 续写故事,创造一个转折点...',
'presets': [
{'id': 'turning_point', 'title': '转折点', 'content': '创造一个重要的转折点,改变故事走向。'},
{'id': 'character_growth', 'title': '角色成长', 'content': '展现角色的内心成长和变化。'},
{'id': 'conflict_escalation', 'title': '冲突升级', 'content': '加剧现有冲突,增强戏剧张力。'},
{'id': 'revelation', 'title': '重要揭示', 'content': '揭示重要信息或秘密,推动情节发展。'},
],
},
),
const FormFieldConfig(
type: AIFormFieldType.length,
title: '长度',
description: '生成内容的字数',
isRequired: true,
options: {
'radioOptions': [
{'value': '200', 'label': '200字'},
{'value': '400', 'label': '400字'},
{'value': '600', 'label': '600字'},
],
'placeholder': 'e.g. 500',
},
),
const FormFieldConfig(
type: AIFormFieldType.contextSelection,
title: '附加上下文',
description: '为AI提供的任何额外信息',
),
const FormFieldConfig(
type: AIFormFieldType.smartContext,
title: '智能上下文',
description: '使用AI自动检索相关背景信息提升生成质量',
),
const FormFieldConfig(
type: AIFormFieldType.promptTemplate,
title: '关联提示词模板',
description: '选择要关联的提示词模板(可选)',
),
const FormFieldConfig(
type: AIFormFieldType.temperature,
title: '温度',
description: '控制生成内容的创造性',
),
const FormFieldConfig(
type: AIFormFieldType.topP,
title: 'Top-P',
description: '控制生成内容的多样性',
),
const FormFieldConfig(
type: AIFormFieldType.quickAccess,
title: '快捷访问',
description: '是否在功能对话框中显示此预设',
),
],
// 🚀 新增:写作编排(大纲/章节/组合)
AIFeatureType.novelCompose: [
const FormFieldConfig(
type: AIFormFieldType.instructions,
title: '指令',
description: '为AI提供写作编排的总体目标如风格、体裁、读者定位等',
options: {
'placeholder': 'e.g. 悬疑+家庭剧的现代都市小说目标读者18-35节奏偏快',
},
),
const FormFieldConfig(
type: AIFormFieldType.contextSelection,
title: '附加上下文',
description: '为AI提供的任何额外信息设定、摘要、章节等',
),
const FormFieldConfig(
type: AIFormFieldType.smartContext,
title: '智能上下文',
description: '使用AI自动检索相关背景信息提升编排质量',
),
const FormFieldConfig(
type: AIFormFieldType.promptTemplate,
title: '关联提示词模板',
description: '选择要关联的提示词模板(可选)',
),
const FormFieldConfig(
type: AIFormFieldType.temperature,
title: '温度',
description: '控制生成内容的创造性',
),
const FormFieldConfig(
type: AIFormFieldType.topP,
title: 'Top-P',
description: '控制生成内容的多样性',
),
const FormFieldConfig(
type: AIFormFieldType.quickAccess,
title: '快捷访问',
description: '是否在功能对话框中显示此预设',
),
],
};
/// 获取指定AI功能类型的表单配置
static List<FormFieldConfig> getFormConfig(AIFeatureType featureType) {
return _configs[featureType] ?? [];
}
/// 获取指定AI功能类型的表单配置通过字符串
static List<FormFieldConfig> getFormConfigByString(String featureTypeString) {
try {
final featureType = AIFeatureTypeHelper.fromApiString(featureTypeString.toUpperCase());
return getFormConfig(featureType);
} catch (e) {
return [];
}
}
/// 检查指定功能类型是否包含某个字段
static bool hasField(AIFeatureType featureType, AIFormFieldType fieldType) {
final config = getFormConfig(featureType);
return config.any((field) => field.type == fieldType);
}
/// 获取指定功能类型的指定字段配置
static FormFieldConfig? getFieldConfig(AIFeatureType featureType, AIFormFieldType fieldType) {
final config = getFormConfig(featureType);
try {
return config.firstWhere((field) => field.type == fieldType);
} catch (e) {
return null;
}
}
}

View File

@@ -0,0 +1,123 @@
import 'package:ainoval/models/model_info.dart'; // Import ModelInfo
import 'package:meta/meta.dart';
/// AI模型分组模型用于UI显示
@immutable
class AIModelGroup {
const AIModelGroup({
required this.provider,
required this.groups,
});
final String provider;
final List<ModelPrefixGroup> groups;
/// 从 ModelInfo 列表创建分组
factory AIModelGroup.fromModelInfoList(String provider, List<ModelInfo> models) {
final Map<String, List<ModelInfo>> groupedModels = {};
for (final modelInfo in models) {
String prefix;
// Use model ID for prefix extraction
final modelId = modelInfo.id;
if (modelId.contains('/')) {
prefix = modelId.split('/').first;
} else if (modelId.contains(':')) {
prefix = modelId.split(':').first;
} else if (modelId.contains('-')) {
final parts = modelId.split('-');
prefix = parts.first;
} else {
prefix = modelId;
}
if (!groupedModels.containsKey(prefix)) {
groupedModels[prefix] = [];
}
groupedModels[prefix]!.add(modelInfo);
}
final groups = groupedModels.entries
.map((entry) => ModelPrefixGroup(
prefix: entry.key,
// Pass ModelInfo list to ModelPrefixGroup constructor
modelsInfo: entry.value,
))
.toList();
groups.sort((a, b) => a.prefix.compareTo(b.prefix));
return AIModelGroup(
provider: provider,
groups: groups,
);
}
/// 获取所有模型的平铺列表
List<ModelInfo> get allModelsInfo {
final List<ModelInfo> result = [];
for (final group in groups) {
result.addAll(group.modelsInfo);
}
return result;
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is AIModelGroup &&
other.provider == provider &&
_listEquals(other.groups, groups);
}
@override
int get hashCode => provider.hashCode ^ Object.hashAll(groups);
// 辅助方法:比较两个列表是否相等
bool _listEquals<T>(List<T>? a, List<T>? b) {
if (a == null) return b == null;
if (b == null || a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
}
/// 按前缀分组的模型
@immutable
class ModelPrefixGroup {
const ModelPrefixGroup({
required this.prefix,
required this.modelsInfo, // Change from models (List<String>)
});
final String prefix;
final List<ModelInfo> modelsInfo; // Store ModelInfo
// Keep models getter for backward compatibility or UI that needs strings?
List<String> get models => modelsInfo.map((info) => info.id).toList();
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ModelPrefixGroup &&
other.prefix == prefix &&
_listEquals(other.modelsInfo, modelsInfo); // Compare ModelInfo lists
}
@override
int get hashCode => prefix.hashCode ^ Object.hashAll(modelsInfo);
// 辅助方法:比较两个列表是否相等
bool _listEquals<T>(List<T>? a, List<T>? b) {
if (a == null) return b == null;
if (b == null || a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
}

View File

@@ -0,0 +1,680 @@
import 'package:ainoval/models/context_selection_models.dart';
import 'package:ainoval/models/user_ai_model_config_model.dart';
import 'package:ainoval/utils/date_time_parser.dart';
/// AI请求类型枚举
enum AIRequestType {
chat('AI_CHAT', '聊天对话'),
expansion('TEXT_EXPANSION', '扩写文本'),
summary('TEXT_SUMMARY', '缩写文本'),
sceneSummary('SCENE_TO_SUMMARY', '场景摘要'),
refactor('TEXT_REFACTOR', '重构文本'),
generation('NOVEL_GENERATION', '内容生成'),
sceneBeat('SCENE_BEAT_GENERATION', '场景节拍生成'),
novelCompose('NOVEL_COMPOSE', '设定编排');
const AIRequestType(this.value, this.displayName);
final String value;
final String displayName;
}
/// 通用AI请求模型
class UniversalAIRequest {
const UniversalAIRequest({
required this.requestType,
required this.userId,
this.sessionId,
this.novelId,
this.chapterId,
this.sceneId,
this.settingSessionId,
this.modelConfig,
this.prompt,
this.instructions,
this.selectedText,
this.contextSelections,
this.enableSmartContext = false,
this.parameters = const {},
this.metadata = const {},
});
/// 请求类型
final AIRequestType requestType;
/// 用户ID
final String userId;
/// 会话ID聊天对话时必填
final String? sessionId;
/// 小说ID
final String? novelId;
/// 章节ID用于上下文提供器
final String? chapterId;
/// 场景ID用于上下文提供器
final String? sceneId;
/// 设定生成会话ID用于设定编排/写作编排场景)
final String? settingSessionId;
/// 模型配置
final UserAIModelConfigModel? modelConfig;
/// 主要提示内容(用户输入的消息或待处理的文本)
final String? prompt;
/// 指令内容AI执行任务的具体指导
final String? instructions;
/// 选中的文本(扩写、缩写、重构时使用)
final String? selectedText;
/// 上下文选择数据
final ContextSelectionData? contextSelections;
/// 是否启用智能上下文RAG检索
final bool enableSmartContext;
/// 请求参数温度、最大token等
final Map<String, dynamic> parameters;
/// 元数据(其他附加信息)
final Map<String, dynamic> metadata;
/// 复制方法
UniversalAIRequest copyWith({
AIRequestType? requestType,
String? userId,
String? sessionId,
String? novelId,
String? chapterId,
String? sceneId,
String? settingSessionId,
UserAIModelConfigModel? modelConfig,
String? prompt,
String? instructions,
String? selectedText,
ContextSelectionData? contextSelections,
bool? enableSmartContext,
Map<String, dynamic>? parameters,
Map<String, dynamic>? metadata,
}) {
return UniversalAIRequest(
requestType: requestType ?? this.requestType,
userId: userId ?? this.userId,
sessionId: sessionId ?? this.sessionId,
novelId: novelId ?? this.novelId,
chapterId: chapterId ?? this.chapterId,
sceneId: sceneId ?? this.sceneId,
settingSessionId: settingSessionId ?? this.settingSessionId,
modelConfig: modelConfig ?? this.modelConfig,
prompt: prompt ?? this.prompt,
instructions: instructions ?? this.instructions,
selectedText: selectedText ?? this.selectedText,
contextSelections: contextSelections ?? this.contextSelections,
enableSmartContext: enableSmartContext ?? this.enableSmartContext,
parameters: parameters ?? this.parameters,
metadata: metadata ?? this.metadata,
);
}
/// 转换为API请求的JSON格式
Map<String, dynamic> toApiJson() {
final Map<String, dynamic> json = {
'requestType': requestType.value,
'userId': userId,
'enableSmartContext': enableSmartContext,
};
// 添加可选字段
if (sessionId != null) json['sessionId'] = sessionId;
if (novelId != null) json['novelId'] = novelId;
if (chapterId != null) json['chapterId'] = chapterId;
if (sceneId != null) json['sceneId'] = sceneId;
if (settingSessionId != null) json['settingSessionId'] = settingSessionId;
if (prompt != null) json['prompt'] = prompt;
if (instructions != null) json['instructions'] = instructions;
if (selectedText != null) json['selectedText'] = selectedText;
// 模型配置
if (modelConfig != null) {
json['modelName'] = modelConfig!.modelName;
json['modelProvider'] = modelConfig!.provider;
final bool isPublic = metadata['isPublicModel'] == true;
// 仅在私有模型时发送 modelConfigId避免公共模型被误判为私有配置查询
if (!isPublic) {
json['modelConfigId'] = modelConfig!.id;
}
// 🚀 明确标识是否为公共模型并传递公共配置ID
if (isPublic) {
json['isPublicModel'] = true;
if (metadata.containsKey('publicModelConfigId') && metadata['publicModelConfigId'] != null) {
// 优先使用 publicModelConfigId与后端期望一致
json['publicModelConfigId'] = metadata['publicModelConfigId'];
}
if (metadata.containsKey('publicModelId') && metadata['publicModelId'] != null) {
json['publicModelId'] = metadata['publicModelId']; // 兼容旧字段
}
print('🔧 [UniversalAIRequest.toApiJson] 公共模型请求 - 模型: ${modelConfig!.modelName}, 提供商: ${modelConfig!.provider}, 公共模型ID: ${metadata['publicModelId'] ?? metadata['publicModelConfigId']}');
} else {
json['isPublicModel'] = false;
print('🔧 [UniversalAIRequest.toApiJson] 私有模型请求 - 模型: ${modelConfig!.modelName}, 提供商: ${modelConfig!.provider}, 配置ID: ${modelConfig!.id}');
}
}
// 上下文选择
if (contextSelections != null && contextSelections!.selectedCount > 0) {
final contextList = contextSelections!.selectedItems.values
.map((item) => {
'id': item.id,
'title': item.title,
'type': item.type.value, // 🚀 修复使用API值而不是displayName
'metadata': item.metadata,
})
.toList();
json['contextSelections'] = contextList;
// 🚀 添加调试日志
print('🔧 [UniversalAIRequest.toApiJson] 添加上下文选择: ${contextList.length}个项目');
for (var item in contextList) {
print(' - ${item['type']}:${item['id']} (${item['title']})');
}
} else {
print('🔧 [UniversalAIRequest.toApiJson] 没有上下文选择数据');
}
// 请求参数
json['parameters'] = {
'temperature': parameters['temperature'] ?? 0.7,
'maxTokens': parameters['maxTokens'] ?? 2000,
'enableSmartContext': enableSmartContext, // 🚀 确保enableSmartContext也在parameters中
...parameters,
};
// 元数据
if (metadata.isNotEmpty) {
json['metadata'] = metadata;
}
return json;
}
/// 从JSON创建请求对象
factory UniversalAIRequest.fromJson(Map<String, dynamic> json) {
// 🚀 处理contextSelections字段
ContextSelectionData? contextSelections;
if (json['contextSelections'] != null) {
final contextList = json['contextSelections'] as List<dynamic>;
print('🔧 [UniversalAIRequest.fromJson] 解析contextSelections: ${contextList.length}个项目');
// 🚀 新增:检查是否需要过滤预设模板上下文
final isPresetTemplate = json['metadata']?['isPresetTemplate'] == true ||
json['source'] == 'preset_template' ||
contextList.any((item) => item['metadata']?['isHardcoded'] == true);
if (isPresetTemplate) {
print('🔧 [UniversalAIRequest.fromJson] 检测到预设模板,启用上下文过滤');
}
// 将已选择的项目转换为ContextSelectionItem并标记为已选择
final selectedItems = <String, ContextSelectionItem>{};
final availableItems = <ContextSelectionItem>[];
final flatItems = <String, ContextSelectionItem>{};
for (var itemData in contextList) {
final contextType = itemData['type'] as String?;
// 🚀 预设模板上下文过滤:只保留硬编码的上下文类型
if (isPresetTemplate && !_isHardcodedContextType(contextType)) {
print(' 🚫 过滤掉非硬编码上下文: $contextType');
continue;
}
final item = ContextSelectionItem(
id: itemData['id'] ?? '',
title: itemData['title'] ?? '',
type: ContextSelectionType.values.firstWhere(
(type) => type.value == itemData['type'],
orElse: () => ContextSelectionType.fullNovelText,
),
metadata: Map<String, dynamic>.from(itemData['metadata'] ?? {}),
parentId: itemData['parentId'],
selectionState: SelectionState.fullySelected, // 标记为已选择
);
selectedItems[item.id] = item;
availableItems.add(item);
flatItems[item.id] = item;
print('${item.type.displayName}:${item.id} (${item.title})');
}
// 创建ContextSelectionData包含选择状态
contextSelections = ContextSelectionData(
novelId: json['novelId'] ?? '',
selectedItems: selectedItems,
availableItems: availableItems,
flatItems: flatItems,
);
if (isPresetTemplate) {
print('🔧 [UniversalAIRequest.fromJson] 预设模板上下文过滤完成: ${contextSelections.selectedCount}个硬编码项目');
} else {
print('🔧 [UniversalAIRequest.fromJson] 创建ContextSelectionData: ${contextSelections.selectedCount}个已选择项目');
}
}
// 🚀 智能获取enableSmartContext优先从顶级字段获取回退到parameters中获取
final Map<String, dynamic> parameters = Map<String, dynamic>.from(json['parameters'] ?? {});
bool enableSmartContext = json['enableSmartContext'] ??
parameters['enableSmartContext'] ??
false;
return UniversalAIRequest(
requestType: AIRequestType.values.firstWhere(
(type) => type.value == json['requestType'],
orElse: () => AIRequestType.chat,
),
userId: json['userId'] ?? '',
sessionId: json['sessionId'],
novelId: json['novelId'],
chapterId: json['chapterId'],
sceneId: json['sceneId'],
settingSessionId: json['settingSessionId'],
prompt: json['prompt'],
instructions: json['instructions'],
selectedText: json['selectedText'],
contextSelections: contextSelections,
enableSmartContext: enableSmartContext,
parameters: parameters,
metadata: Map<String, dynamic>.from(json['metadata'] ?? {}),
);
}
/// 🚀 新增:判断是否为硬编码的预设模板上下文类型
static bool _isHardcodedContextType(String? contextType) {
if (contextType == null) return false;
// 定义预设模板允许的硬编码上下文类型
const hardcodedTypes = {
// 核心文本上下文
'full_novel_text', // 全文文本
'full_outline', // 完整大纲
'novel_basic_info', // 基本信息
// 前五章相关
'recent_chapters_content', // 前五章内容
'recent_chapters_summary', // 前五章摘要
// 结构化上下文
'settings', // 设定
'snippets', // 片段
// 当前上下文
'chapters', // 章节(当前章节)
'scenes', // 场景(当前场景)
// 世界观相关
'setting_groups', // 设定组
'codex_entries', // 词条
};
return hardcodedTypes.contains(contextType);
}
}
/// AI响应模型
class UniversalAIResponse {
const UniversalAIResponse({
required this.id,
required this.requestType,
required this.content,
this.finishReason,
this.tokenUsage,
this.model,
this.createdAt,
this.metadata = const {},
});
/// 响应ID
final String id;
/// 对应的请求类型
final AIRequestType requestType;
/// 生成的内容
final String content;
/// 完成原因
final String? finishReason;
/// Token使用情况
final TokenUsage? tokenUsage;
/// 使用的模型
final String? model;
/// 创建时间
final DateTime? createdAt;
/// 元数据
final Map<String, dynamic> metadata;
/// 从JSON创建响应对象
factory UniversalAIResponse.fromJson(Map<String, dynamic> json) {
return UniversalAIResponse(
id: json['id'] ?? '',
requestType: AIRequestType.values.firstWhere(
(type) => type.value == json['requestType'],
orElse: () => AIRequestType.chat,
),
content: json['content'] ?? '',
finishReason: json['finishReason'],
tokenUsage: json['tokenUsage'] != null
? TokenUsage.fromJson(json['tokenUsage'])
: null,
model: json['model'],
createdAt: json['createdAt'] != null
? parseBackendDateTime(json['createdAt'])
: null,
metadata: Map<String, dynamic>.from(json['metadata'] ?? {}),
);
}
/// 转换为JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'requestType': requestType.value,
'content': content,
'finishReason': finishReason,
'tokenUsage': tokenUsage?.toJson(),
'model': model,
'createdAt': createdAt?.toIso8601String(),
'metadata': metadata,
};
}
}
/// Token使用情况
class TokenUsage {
const TokenUsage({
this.promptTokens = 0,
this.completionTokens = 0,
this.totalTokens = 0,
});
final int promptTokens;
final int completionTokens;
final int totalTokens;
/// 从JSON创建Token使用情况
factory TokenUsage.fromJson(Map<String, dynamic> json) {
return TokenUsage(
promptTokens: json['promptTokens'] ?? 0,
completionTokens: json['completionTokens'] ?? 0,
totalTokens: json['totalTokens'] ?? 0,
);
}
/// 转换为JSON
Map<String, dynamic> toJson() {
return {
'promptTokens': promptTokens,
'completionTokens': completionTokens,
'totalTokens': totalTokens,
};
}
}
/// 通用AI预览响应模型
class UniversalAIPreviewResponse {
const UniversalAIPreviewResponse({
required this.preview,
required this.systemPrompt,
required this.userPrompt,
this.context,
this.estimatedTokens,
this.modelName,
this.modelProvider,
this.modelConfigId,
});
/// 预览内容(完整的提示词)
final String preview;
/// 系统提示词
final String systemPrompt;
/// 用户提示词
final String userPrompt;
/// 上下文信息
final String? context;
/// 估计的Token数量
final int? estimatedTokens;
/// 将要使用的模型名称
final String? modelName;
/// 将要使用的模型提供商
final String? modelProvider;
/// 模型配置ID
final String? modelConfigId;
/// 从JSON创建预览响应
factory UniversalAIPreviewResponse.fromJson(Map<String, dynamic> json) {
return UniversalAIPreviewResponse(
preview: json['preview'] ?? '',
systemPrompt: json['systemPrompt'] ?? '',
userPrompt: json['userPrompt'] ?? '',
context: json['context'],
estimatedTokens: json['estimatedTokens'],
modelName: json['modelName'],
modelProvider: json['modelProvider'],
modelConfigId: json['modelConfigId'],
);
}
/// 转换为JSON
Map<String, dynamic> toJson() {
return {
'preview': preview,
'systemPrompt': systemPrompt,
'userPrompt': userPrompt,
'context': context,
'estimatedTokens': estimatedTokens,
'modelName': modelName,
'modelProvider': modelProvider,
'modelConfigId': modelConfigId,
};
}
/// 计算系统提示词的字数
int get systemPromptWordCount => _countWords(systemPrompt);
/// 计算用户提示词的字数
int get userPromptWordCount => _countWords(userPrompt);
/// 计算上下文的字数
int get contextWordCount => context != null ? _countWords(context!) : 0;
/// 计算总字数
int get totalWordCount => systemPromptWordCount + userPromptWordCount + contextWordCount;
/// 计算字数的辅助方法
static int _countWords(String text) {
if (text.isEmpty) return 0;
// 简单的字数计算:按空格分割英文单词,中文字符直接计数
int wordCount = 0;
int chineseCharCount = 0;
// 分割文本按空格
final words = text.split(RegExp(r'\s+'));
for (String word in words) {
if (word.trim().isEmpty) continue;
// 计算中文字符
for (int i = 0; i < word.length; i++) {
final charCode = word.codeUnitAt(i);
if (charCode >= 0x4e00 && charCode <= 0x9fff) {
chineseCharCount++;
}
}
// 移除中文字符后计算英文单词
final nonChineseWord = word.replaceAll(RegExp(r'[\u4e00-\u9fff]'), '');
if (nonChineseWord.trim().isNotEmpty) {
wordCount++;
}
}
// 中文字符每个算一个词,英文单词按原数量
return wordCount + chineseCharCount;
}
}
/// 扩展上下文选择类型枚举添加value字段用于API传输
extension ContextSelectionTypeApi on ContextSelectionType {
String get value {
switch (this) {
case ContextSelectionType.fullNovelText:
return 'full_novel_text';
case ContextSelectionType.fullOutline:
return 'full_outline';
case ContextSelectionType.novelBasicInfo:
return 'novel_basic_info';
case ContextSelectionType.recentChaptersContent:
return 'recent_chapters_content';
case ContextSelectionType.recentChaptersSummary:
return 'recent_chapters_summary';
case ContextSelectionType.currentSceneContent:
return 'current_scene_content';
case ContextSelectionType.currentSceneSummary:
return 'current_scene_summary';
case ContextSelectionType.currentChapterContent:
return 'current_chapter_content';
case ContextSelectionType.currentChapterSummaries:
return 'current_chapter_summary';
case ContextSelectionType.previousChaptersContent:
return 'previous_chapters_content';
case ContextSelectionType.previousChaptersSummary:
return 'previous_chapters_summary';
case ContextSelectionType.contentFixedGroup:
case ContextSelectionType.summaryFixedGroup:
return 'group';
case ContextSelectionType.acts:
return 'acts';
case ContextSelectionType.chapters:
return 'chapters';
case ContextSelectionType.scenes:
return 'scenes';
case ContextSelectionType.snippets:
return 'snippets';
case ContextSelectionType.settings:
return 'settings';
case ContextSelectionType.settingGroups:
return 'setting_groups';
case ContextSelectionType.settingsByType:
return 'settings_by_type';
case ContextSelectionType.codexEntries:
return 'codex_entries';
case ContextSelectionType.entriesByType:
return 'entries_by_type';
case ContextSelectionType.entriesByDetail:
return 'entries_by_detail';
case ContextSelectionType.entriesByCategory:
return 'entries_by_category';
case ContextSelectionType.entriesByTag:
return 'entries_by_tag';
}
}
}
/// 🚀 积分预估响应模型
class CostEstimationResponse {
const CostEstimationResponse({
required this.estimatedCost,
required this.success,
this.errorMessage,
this.estimatedInputTokens,
this.estimatedOutputTokens,
this.costMultiplier,
this.modelName,
this.modelProvider,
this.isPublicModel = false,
this.featureType,
});
/// 预估的积分成本
final int estimatedCost;
/// 是否成功
final bool success;
/// 错误信息
final String? errorMessage;
/// 预估输入Token数量
final int? estimatedInputTokens;
/// 预估输出Token数量
final int? estimatedOutputTokens;
/// 成本倍率
final double? costMultiplier;
/// 模型名称
final String? modelName;
/// 模型提供商
final String? modelProvider;
/// 是否为公共模型
final bool isPublicModel;
/// 功能类型
final String? featureType;
/// 从JSON创建积分预估响应
factory CostEstimationResponse.fromJson(Map<String, dynamic> json) {
return CostEstimationResponse(
estimatedCost: json['estimatedCost']?.toInt() ?? 0,
success: json['success'] ?? false,
errorMessage: json['errorMessage'],
estimatedInputTokens: json['estimatedInputTokens']?.toInt(),
estimatedOutputTokens: json['estimatedOutputTokens']?.toInt(),
costMultiplier: json['costMultiplier']?.toDouble(),
modelName: json['modelName'],
modelProvider: json['modelProvider'],
isPublicModel: json['isPublicModel'] ?? false,
featureType: json['featureType'],
);
}
/// 转换为JSON
Map<String, dynamic> toJson() {
return {
'estimatedCost': estimatedCost,
'success': success,
'errorMessage': errorMessage,
'estimatedInputTokens': estimatedInputTokens,
'estimatedOutputTokens': estimatedOutputTokens,
'costMultiplier': costMultiplier,
'modelName': modelName,
'modelProvider': modelProvider,
'isPublicModel': isPublicModel,
'featureType': featureType,
};
}
}

View File

@@ -0,0 +1,213 @@
import 'package:ainoval/utils/date_time_parser.dart';
class AnalyticsData {
final int totalWords;
final int totalTokens;
final int functionUsageCount;
final int writingDays;
final int monthlyNewWords;
final int monthlyNewTokens;
final int consecutiveDays;
final String mostPopularFunction;
const AnalyticsData({
required this.totalWords,
required this.totalTokens,
required this.functionUsageCount,
required this.writingDays,
required this.monthlyNewWords,
required this.monthlyNewTokens,
required this.consecutiveDays,
required this.mostPopularFunction,
});
factory AnalyticsData.fromJson(Map<String, dynamic> json) {
return AnalyticsData(
totalWords: json['totalWords'] ?? 0,
totalTokens: json['totalTokens'] ?? 0,
functionUsageCount: json['functionUsageCount'] ?? 0,
writingDays: json['writingDays'] ?? 0,
monthlyNewWords: json['monthlyNewWords'] ?? 0,
monthlyNewTokens: json['monthlyNewTokens'] ?? 0,
consecutiveDays: json['consecutiveDays'] ?? 0,
mostPopularFunction: json['mostPopularFunction'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'totalWords': totalWords,
'totalTokens': totalTokens,
'functionUsageCount': functionUsageCount,
'writingDays': writingDays,
'monthlyNewWords': monthlyNewWords,
'monthlyNewTokens': monthlyNewTokens,
'consecutiveDays': consecutiveDays,
'mostPopularFunction': mostPopularFunction,
};
}
}
class TokenUsageData {
final String date;
final int inputTokens;
final int outputTokens;
final int totalTokens;
final Map<String, int> modelTokens; // 按模型名聚合的tokens
const TokenUsageData({
required this.date,
required this.inputTokens,
required this.outputTokens,
required this.totalTokens,
required this.modelTokens,
});
factory TokenUsageData.fromJson(Map<String, dynamic> json) {
return TokenUsageData(
date: json['date'] ?? '',
inputTokens: json['inputTokens'] ?? 0,
outputTokens: json['outputTokens'] ?? 0,
totalTokens: json['totalTokens'] ?? 0,
modelTokens: Map<String, int>.from(json['modelTokens'] ?? {}),
);
}
Map<String, dynamic> toJson() {
return {
'date': date,
'inputTokens': inputTokens,
'outputTokens': outputTokens,
'totalTokens': totalTokens,
'modelTokens': modelTokens,
};
}
}
class FunctionUsageData {
final String name;
final int value;
final double growth; // 增长率百分比
const FunctionUsageData({
required this.name,
required this.value,
required this.growth,
});
factory FunctionUsageData.fromJson(Map<String, dynamic> json) {
return FunctionUsageData(
name: json['name'] ?? '',
value: json['value'] ?? 0,
growth: (json['growth'] ?? 0.0).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'value': value,
'growth': growth,
};
}
}
class ModelUsageData {
final String modelName;
final int percentage;
final int totalTokens;
final String color;
const ModelUsageData({
required this.modelName,
required this.percentage,
required this.totalTokens,
required this.color,
});
factory ModelUsageData.fromJson(Map<String, dynamic> json) {
return ModelUsageData(
modelName: json['modelName'] ?? '',
percentage: json['percentage'] ?? 0,
totalTokens: json['totalTokens'] ?? 0,
color: json['color'] ?? '#000000',
);
}
Map<String, dynamic> toJson() {
return {
'modelName': modelName,
'percentage': percentage,
'totalTokens': totalTokens,
'color': color,
};
}
}
class TokenUsageRecord {
final String id;
final DateTime timestamp;
final int inputTokens;
final int outputTokens;
final String model;
final String taskType;
final double cost;
const TokenUsageRecord({
required this.id,
required this.timestamp,
required this.inputTokens,
required this.outputTokens,
required this.model,
required this.taskType,
required this.cost,
});
factory TokenUsageRecord.fromJson(Map<String, dynamic> json) {
return TokenUsageRecord(
id: json['id'] ?? '',
timestamp: parseBackendDateTime(json['timestamp']),
inputTokens: json['inputTokens'] ?? 0,
outputTokens: json['outputTokens'] ?? 0,
model: json['model'] ?? '',
taskType: json['taskType'] ?? '',
cost: (json['cost'] ?? 0.0).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'timestamp': timestamp.toIso8601String(),
'inputTokens': inputTokens,
'outputTokens': outputTokens,
'model': model,
'taskType': taskType,
'cost': cost,
};
}
int get totalTokens => inputTokens + outputTokens;
}
enum AnalyticsViewMode {
daily,
monthly,
cumulative,
range,
}
extension AnalyticsViewModeExtension on AnalyticsViewMode {
String get displayName {
switch (this) {
case AnalyticsViewMode.daily:
return '按天';
case AnalyticsViewMode.monthly:
return '按月';
case AnalyticsViewMode.cumulative:
return '累计';
case AnalyticsViewMode.range:
return '日期范围';
}
}
}

View File

@@ -0,0 +1,67 @@
import 'package:json_annotation/json_annotation.dart';
/// 场景摘要生成请求 DTO
class SummarizeSceneRequest {
final String? additionalInstructions;
SummarizeSceneRequest({
this.additionalInstructions,
});
Map<String, dynamic> toJson() {
return {
if (additionalInstructions != null) 'additionalInstructions': additionalInstructions,
};
}
}
/// 场景摘要生成响应 DTO
class SummarizeSceneResponse {
final String summary;
SummarizeSceneResponse({
required this.summary,
});
factory SummarizeSceneResponse.fromJson(Map<String, dynamic> json) {
return SummarizeSceneResponse(
summary: json['summary'] as String,
);
}
}
/// 从摘要生成场景请求 DTO
class GenerateSceneFromSummaryRequest {
final String summary;
final String? chapterId;
final String? additionalInstructions;
GenerateSceneFromSummaryRequest({
required this.summary,
this.chapterId,
this.additionalInstructions,
});
Map<String, dynamic> toJson() {
return {
'summary': summary,
if (chapterId != null) 'chapterId': chapterId,
if (additionalInstructions != null) 'additionalInstructions': additionalInstructions,
};
}
}
/// 从摘要生成场景响应 DTO
class GenerateSceneFromSummaryResponse {
final String content;
GenerateSceneFromSummaryResponse({
required this.content,
});
factory GenerateSceneFromSummaryResponse.fromJson(Map<String, dynamic> json) {
return GenerateSceneFromSummaryResponse(
content: json['content'] as String,
);
}
}

View File

@@ -0,0 +1,207 @@
import 'package:shared_preferences/shared_preferences.dart';
/// 应用注册配置
/// 管理注册功能的开关和设置
class AppRegistrationConfig {
static const String _phoneRegistrationEnabledKey = 'phone_registration_enabled';
static const String _emailRegistrationEnabledKey = 'email_registration_enabled';
static const String _requireVerificationKey = 'require_verification';
static const String _quickRegistrationEnabledKey = 'quick_registration_enabled';
// 默认配置MVP仅快捷注册
static const bool _defaultPhoneRegistrationEnabled = false; // 关闭手机注册
static const bool _defaultEmailRegistrationEnabled = false; // 关闭邮箱注册
static const bool _defaultRequireVerification = false; // 关闭验证码
static const bool _defaultQuickRegistrationEnabled = true; // 开启快捷注册
// 缓存配置
static bool? _cachedPhoneRegistrationEnabled;
static bool? _cachedEmailRegistrationEnabled;
static bool? _cachedRequireVerification;
static bool? _cachedQuickRegistrationEnabled;
/// 获取是否启用快捷注册
static Future<bool> isQuickRegistrationEnabled() async {
if (_cachedQuickRegistrationEnabled != null) {
return _cachedQuickRegistrationEnabled!;
}
final prefs = await SharedPreferences.getInstance();
_cachedQuickRegistrationEnabled = prefs.getBool(_quickRegistrationEnabledKey) ?? _defaultQuickRegistrationEnabled;
return _cachedQuickRegistrationEnabled!;
}
/// 设置是否启用快捷注册
static Future<void> setQuickRegistrationEnabled(bool enabled) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_quickRegistrationEnabledKey, enabled);
_cachedQuickRegistrationEnabled = enabled;
}
/// 获取是否启用手机注册
static Future<bool> isPhoneRegistrationEnabled() async {
if (_cachedPhoneRegistrationEnabled != null) {
return _cachedPhoneRegistrationEnabled!;
}
final prefs = await SharedPreferences.getInstance();
_cachedPhoneRegistrationEnabled = prefs.getBool(_phoneRegistrationEnabledKey) ?? _defaultPhoneRegistrationEnabled;
return _cachedPhoneRegistrationEnabled!;
}
/// 设置是否启用手机注册
static Future<void> setPhoneRegistrationEnabled(bool enabled) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_phoneRegistrationEnabledKey, enabled);
_cachedPhoneRegistrationEnabled = enabled;
}
/// 获取是否启用邮箱注册
static Future<bool> isEmailRegistrationEnabled() async {
if (_cachedEmailRegistrationEnabled != null) {
return _cachedEmailRegistrationEnabled!;
}
final prefs = await SharedPreferences.getInstance();
_cachedEmailRegistrationEnabled = prefs.getBool(_emailRegistrationEnabledKey) ?? _defaultEmailRegistrationEnabled;
return _cachedEmailRegistrationEnabled!;
}
/// 设置是否启用邮箱注册
static Future<void> setEmailRegistrationEnabled(bool enabled) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_emailRegistrationEnabledKey, enabled);
_cachedEmailRegistrationEnabled = enabled;
}
/// 获取是否需要验证
static Future<bool> isVerificationRequired() async {
if (_cachedRequireVerification != null) {
return _cachedRequireVerification!;
}
final prefs = await SharedPreferences.getInstance();
_cachedRequireVerification = prefs.getBool(_requireVerificationKey) ?? _defaultRequireVerification;
return _cachedRequireVerification!;
}
/// 设置是否需要验证
static Future<void> setVerificationRequired(bool required) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_requireVerificationKey, required);
_cachedRequireVerification = required;
}
/// 获取可用的注册方式列表
static Future<List<RegistrationMethod>> getAvailableRegistrationMethods() async {
final List<RegistrationMethod> methods = [];
if (await isEmailRegistrationEnabled()) {
methods.add(RegistrationMethod.email);
}
if (await isPhoneRegistrationEnabled()) {
methods.add(RegistrationMethod.phone);
}
return methods;
}
/// 检查是否至少有一种注册方式可用
static Future<bool> hasAvailableRegistrationMethod() async {
final methods = await getAvailableRegistrationMethods();
return methods.isNotEmpty;
}
/// 重置所有配置到默认值
static Future<void> resetToDefaults() async {
await setPhoneRegistrationEnabled(_defaultPhoneRegistrationEnabled);
await setEmailRegistrationEnabled(_defaultEmailRegistrationEnabled);
await setVerificationRequired(_defaultRequireVerification);
await setQuickRegistrationEnabled(_defaultQuickRegistrationEnabled);
}
/// 清除缓存
static void clearCache() {
_cachedPhoneRegistrationEnabled = null;
_cachedEmailRegistrationEnabled = null;
_cachedRequireVerification = null;
_cachedQuickRegistrationEnabled = null;
}
}
/// 注册方式枚举
enum RegistrationMethod {
email('邮箱注册', 'email'),
phone('手机注册', 'phone');
const RegistrationMethod(this.displayName, this.value);
final String displayName;
final String value;
}
/// 注册配置数据类
class RegistrationConfig {
const RegistrationConfig({
required this.phoneRegistrationEnabled,
required this.emailRegistrationEnabled,
required this.verificationRequired,
this.quickRegistrationEnabled = true,
});
final bool phoneRegistrationEnabled;
final bool emailRegistrationEnabled;
final bool verificationRequired;
final bool quickRegistrationEnabled;
/// 获取可用的注册方式
List<RegistrationMethod> get availableMethods {
final List<RegistrationMethod> methods = [];
if (emailRegistrationEnabled) {
methods.add(RegistrationMethod.email);
}
if (phoneRegistrationEnabled) {
methods.add(RegistrationMethod.phone);
}
return methods;
}
/// 是否至少有一种注册方式可用
bool get hasAvailableMethod => availableMethods.isNotEmpty;
/// 复制配置
RegistrationConfig copyWith({
bool? phoneRegistrationEnabled,
bool? emailRegistrationEnabled,
bool? verificationRequired,
bool? quickRegistrationEnabled,
}) {
return RegistrationConfig(
phoneRegistrationEnabled: phoneRegistrationEnabled ?? this.phoneRegistrationEnabled,
emailRegistrationEnabled: emailRegistrationEnabled ?? this.emailRegistrationEnabled,
verificationRequired: verificationRequired ?? this.verificationRequired,
quickRegistrationEnabled: quickRegistrationEnabled ?? this.quickRegistrationEnabled,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is RegistrationConfig &&
other.phoneRegistrationEnabled == phoneRegistrationEnabled &&
other.emailRegistrationEnabled == emailRegistrationEnabled &&
other.verificationRequired == verificationRequired &&
other.quickRegistrationEnabled == quickRegistrationEnabled;
}
@override
int get hashCode => Object.hash(
phoneRegistrationEnabled,
emailRegistrationEnabled,
verificationRequired,
quickRegistrationEnabled,
);
}

View File

@@ -0,0 +1,89 @@
import 'novel_structure.dart';
/// 预加载章节数据传输对象
/// 专门用于阅读器预加载功能,包含章节列表和对应的场景内容
class ChaptersForPreloadDto {
const ChaptersForPreloadDto({
required this.chapters,
required this.scenesByChapter,
});
/// 从JSON创建实例
factory ChaptersForPreloadDto.fromJson(Map<String, dynamic> json) {
// 解析章节列表
final List<Chapter> chaptersList = [];
if (json['chapters'] != null && json['chapters'] is List) {
chaptersList.addAll(
(json['chapters'] as List<dynamic>)
.map((chapterJson) => Chapter.fromJson(chapterJson as Map<String, dynamic>))
.toList(),
);
}
// 解析按章节分组的场景
final Map<String, List<Scene>> scenesMap = {};
if (json['scenesByChapter'] != null && json['scenesByChapter'] is Map) {
final rawScenesMap = json['scenesByChapter'] as Map<String, dynamic>;
for (final entry in rawScenesMap.entries) {
final chapterId = entry.key;
final scenesList = <Scene>[];
if (entry.value is List) {
scenesList.addAll(
(entry.value as List<dynamic>)
.map((sceneJson) => Scene.fromJson(sceneJson as Map<String, dynamic>))
.toList(),
);
}
scenesMap[chapterId] = scenesList;
}
}
return ChaptersForPreloadDto(
chapters: chaptersList,
scenesByChapter: scenesMap,
);
}
/// 章节列表,按顺序排列
final List<Chapter> chapters;
/// 按章节ID分组的场景列表
/// Key: 章节ID
/// Value: 该章节的场景列表按sequence排序
final Map<String, List<Scene>> scenesByChapter;
/// 获取章节总数
int get chapterCount => chapters.length;
/// 获取场景总数
int get totalSceneCount {
return scenesByChapter.values
.map((scenes) => scenes.length)
.fold(0, (sum, count) => sum + count);
}
/// 检查是否包含指定章节的数据
bool containsChapter(String chapterId) {
return chapters.any((chapter) => chapter.id == chapterId);
}
/// 转换为JSON
Map<String, dynamic> toJson() {
return {
'chapters': chapters.map((chapter) => chapter.toJson()).toList(),
'scenesByChapter': scenesByChapter.map(
(chapterId, scenes) => MapEntry(
chapterId,
scenes.map((scene) => scene.toJson()).toList(),
),
),
};
}
@override
String toString() {
return 'ChaptersForPreloadDto(chapterCount: $chapterCount, totalSceneCount: $totalSceneCount)';
}
}

View File

@@ -0,0 +1,156 @@
/// 消息发送者枚举
enum MessageSender {
user, // 用户发送的消息
ai, // AI助手发送的消息
}
// 可以为消息状态定义一个枚举
enum MessageStatus {
sending,
sent,
delivered,
read,
error,
unknown, // 处理未知状态
}
// 可以为消息类型定义一个枚举
enum MessageType {
text,
image,
audio,
command,
unknown, // 处理未知类型
}
/// 聊天消息模型
class ChatMessage {
/// 构造函数
ChatMessage({
required this.id,
required this.content,
required this.sender,
required this.timestamp,
// 添加新字段,设为可选,以便旧数据或不需要这些字段的地方能兼容
this.sessionId,
this.status,
this.messageType,
this.metadata,
});
/// 从JSON创建ChatMessage实例
factory ChatMessage.fromJson(Map<String, dynamic> json) {
// 修正:根据后端 'role' 字段映射到 'sender' 枚举
MessageSender sender;
final role = json['role'] as String?;
if (role == 'assistant') {
sender = MessageSender.ai;
} else if (role == 'user') {
sender = MessageSender.user;
} else {
sender = MessageSender.ai; // 或其他默认处理
print("Warning: Unknown message role '$role' received, mapping to 'ai'.");
}
// 解析 status (可选)
MessageStatus? status;
final statusString = json['status'] as String?;
if (statusString != null) {
try {
status = MessageStatus.values.byName(statusString.toLowerCase());
} catch (e) {
status = MessageStatus.unknown;
print("Warning: Unknown message status '$statusString' received.");
}
}
// 解析 messageType (可选)
MessageType? messageType;
final typeString = json['messageType'] as String?;
if (typeString != null) {
try {
messageType = MessageType.values.byName(typeString.toLowerCase());
} catch (e) {
messageType = MessageType.unknown;
print("Warning: Unknown message type '$typeString' received.");
}
}
return ChatMessage(
id: json['id'] as String,
content: json['content'] as String,
sender: sender, // 使用上面转换后的 sender
// 修正:读取 'createdAt' 字段并解析为 DateTime
timestamp: DateTime.parse(json['createdAt'] as String),
// 读取新添加的可选字段
sessionId: json['sessionId'] as String?,
status: status,
messageType: messageType,
metadata: json['metadata'] as Map<String, dynamic>?, // Dart 中通常用 Map<String, dynamic>
);
}
/// 消息唯一标识符
final String id;
/// 消息内容
final String content;
/// 消息发送者
final MessageSender sender;
/// 消息发送时间
final DateTime timestamp;
// --- 新添加的字段 ---
/// 会话ID (可选)
final String? sessionId;
/// 消息状态 (可选)
final MessageStatus? status;
/// 消息类型 (可选)
final MessageType? messageType;
/// 消息元数据 (可选)
final Map<String, dynamic>? metadata;
// --- 结束 ---
/// 将ChatMessage实例转换为JSON
Map<String, dynamic> toJson() {
// 修正:将 sender 枚举映射到后端的 'role' 字符串
String role;
if (sender == MessageSender.ai) {
role = 'assistant';
} else {
role = 'user';
}
// 注意:通常前端发送消息时,不需要发送所有字段给后端
// 比如 status, messageType 可能由后端确定或不需要前端发送
// sessionId 通常在请求的 URL 或其他地方指定,而不是在消息体里
// metadata 可能需要发送
// 这里我们只包含基础字段和 metadata 示例,根据你的 API 设计调整
final data = <String, dynamic>{
'id': id, // id 通常由后端生成,发送时可能不需要或为空
'content': content,
// 修正:使用 'role' 键和映射后的值
'role': role,
// 修正:使用 'createdAt' 键 (或者后端会自己设置时间戳根据API定)
// 'createdAt': timestamp.toIso8601String(), // 如果需要前端指定创建时间
};
// 按需添加其他字段到发送的 JSON 中
if (sessionId != null) {
// 通常 sessionId 不在消息体里发送,而是在 URL 或 DTO 的顶层字段
// data['sessionId'] = sessionId;
}
if (metadata != null) {
data['metadata'] = metadata;
}
// status 和 messageType 通常不由前端指定发送
return data;
}
}

View File

@@ -0,0 +1,456 @@
import 'package:intl/intl.dart';
import 'package:uuid/uuid.dart';
import '../utils/date_time_parser.dart';
// 聊天会话模型
class ChatSession {
ChatSession({
required this.id,
required this.title,
String? selectedModelConfigId,
required this.createdAt,
required this.lastUpdatedAt,
required this.novelId,
this.chapterId,
this.status,
this.messageCount,
this.metadata,
}) : selectedModelConfigId = selectedModelConfigId;
// 从JSON转换方法
factory ChatSession.fromJson(Map<String, dynamic> json) {
// 辅助函数安全地获取和转换 String
String safeString(String key, [String defaultValue = '']) {
return json[key] as String? ?? defaultValue;
}
// 辅助函数安全地获取和解析 DateTime
DateTime safeDateTime(String key, DateTime defaultValue) {
final value = json[key] as String?;
return value != null
? (DateTime.tryParse(value) ?? defaultValue)
: defaultValue;
}
return ChatSession(
// 使用 sessionId 作为 id并提供一个默认空字符串以防万一
id: safeString('sessionId'),
title: safeString('title', '无标题会话'),
selectedModelConfigId: json['selectedModelConfigId'] as String?,
createdAt: parseBackendDateTime(json['createdAt']),
lastUpdatedAt: parseBackendDateTime(json['updatedAt']),
novelId: safeString('novelId'),
chapterId: json['chapterId'] as String?,
status: json['status'] as String?,
messageCount: (json['messageCount'] as num?)?.toInt() ?? 0,
metadata: json['metadata'] as Map<String, dynamic>?,
);
}
final String id;
final String title;
final String? selectedModelConfigId;
final DateTime createdAt;
final DateTime lastUpdatedAt;
final String novelId;
final String? chapterId;
final String? status;
final int? messageCount;
final Map<String, dynamic>? metadata;
// 复制方法,用于创建会话的副本
ChatSession copyWith({
String? id,
String? title,
String? selectedModelConfigId,
DateTime? createdAt,
DateTime? lastUpdatedAt,
String? novelId,
String? chapterId,
String? status,
int? messageCount,
Map<String, dynamic>? metadata,
}) {
return ChatSession(
id: id ?? this.id,
title: title ?? this.title,
selectedModelConfigId:
selectedModelConfigId ?? this.selectedModelConfigId,
createdAt: createdAt ?? this.createdAt,
lastUpdatedAt: lastUpdatedAt ?? this.lastUpdatedAt,
novelId: novelId ?? this.novelId,
chapterId: chapterId ?? this.chapterId,
status: status ?? this.status,
messageCount: messageCount ?? this.messageCount,
metadata: metadata ?? this.metadata,
);
}
// 转换为JSON方法
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'selectedModelConfigId': selectedModelConfigId,
'createdAt': createdAt.toIso8601String(),
'lastUpdatedAt': lastUpdatedAt.toIso8601String(),
'novelId': novelId,
'chapterId': chapterId,
'status': status,
'messageCount': messageCount,
'metadata': metadata,
};
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ChatSession &&
other.id == id &&
other.title == title &&
other.selectedModelConfigId == selectedModelConfigId &&
other.createdAt == createdAt &&
other.lastUpdatedAt == lastUpdatedAt &&
other.novelId == novelId &&
other.chapterId == chapterId &&
other.status == status &&
other.messageCount == messageCount &&
other.metadata == metadata;
}
@override
int get hashCode {
return id.hashCode ^
title.hashCode ^
selectedModelConfigId.hashCode ^
createdAt.hashCode ^
lastUpdatedAt.hashCode ^
novelId.hashCode ^
chapterId.hashCode ^
status.hashCode ^
messageCount.hashCode ^
metadata.hashCode;
}
}
// 聊天消息模型
class ChatMessage {
ChatMessage({
required this.id,
required this.role,
required this.content,
required this.timestamp,
this.status = MessageStatus.sent,
this.actions,
this.sessionId,
this.userId,
this.novelId,
this.modelName,
this.metadata,
required this.sender,
});
// 从JSON转换方法
factory ChatMessage.fromJson(Map<String, dynamic> json) {
// --- Helper for safe string parsing ---
String safeString(String key, [String defaultValue = '']) {
final value = json[key];
if (value is String) return value;
// Log or handle non-string/null cases if needed
// AppLogger.w('ChatMessage.fromJson', 'Expected String for key "$key", but got ${value?.runtimeType}. Using default.');
return defaultValue;
}
List<MessageAction>? parsedActions;
if (json['metadata'] != null && json['metadata']['actions'] is List) {
parsedActions = (json['metadata']['actions'] as List)
.map((e) => MessageAction.fromJson(e as Map<String, dynamic>))
.toList();
}
// --- Handle potentially null 'id' safely ---
// Provide a temporary unique default if 'id' is null.
// The Bloc logic will prioritize its own placeholder ID anyway.
final messageId = safeString('id', 'temp_chunk_${const Uuid().v4()}');
return ChatMessage(
id: messageId, // Use the safe ID
role: MessageRole.values.firstWhere(
// Use safeString for role
(e) =>
e.name == safeString('role', MessageRole.system.name).toLowerCase(),
orElse: () => MessageRole.system, // Fallback
),
// Use safeString for content, important for potentially empty chunks
content: safeString('content'),
// Assume parseBackendDateTime handles the list format or null
// Provide a fallback default DateTime if key is missing or parsing fails
timestamp: parseBackendDateTime(json['createdAt'] ?? DateTime.now()),
status: MessageStatus.values.firstWhere(
// Use safeString for status
(e) =>
e.name ==
safeString('status', MessageStatus.sent.name).toLowerCase(),
orElse: () => MessageStatus.sent, // Fallback
),
actions: parsedActions,
// These fields allow null, direct access is relatively safe but casting is good practice
sessionId: json['sessionId'] as String?,
userId: json['userId'] as String?,
novelId: json['novelId'] as String?,
modelName: json['modelName'] as String?,
metadata: json['metadata'] as Map<String, dynamic>?,
sender: MessageSender.values.firstWhere(
(e) =>
e.name ==
safeString('sender', MessageSender.user.name).toLowerCase(),
orElse: () => MessageSender.user,
),
);
}
final String id;
final MessageRole role;
final String content;
final DateTime timestamp;
final MessageStatus status;
final List<MessageAction>? actions;
final String? sessionId;
final String? userId;
final String? novelId;
final String? modelName;
final Map<String, dynamic>? metadata;
final MessageSender sender;
// 复制方法
ChatMessage copyWith({
String? id,
MessageRole? role,
String? content,
DateTime? timestamp,
MessageStatus? status,
List<MessageAction>? actions,
String? sessionId,
String? userId,
String? novelId,
String? modelName,
Map<String, dynamic>? metadata,
MessageSender? sender,
}) {
return ChatMessage(
id: id ?? this.id,
role: role ?? this.role,
content: content ?? this.content,
timestamp: timestamp ?? this.timestamp,
status: status ?? this.status,
actions: actions ?? this.actions,
sessionId: sessionId ?? this.sessionId,
userId: userId ?? this.userId,
novelId: novelId ?? this.novelId,
modelName: modelName ?? this.modelName,
metadata: metadata ?? this.metadata,
sender: sender ?? this.sender,
);
}
// 转换为JSON方法
Map<String, dynamic> toJson() {
final Map<String, dynamic> currentMetadata = Map.from(metadata ?? {});
if (actions != null) {
currentMetadata['actions'] = actions!.map((e) => e.toJson()).toList();
}
return {
'id': id,
'role': role.name,
'content': content,
'createdAt': timestamp.toIso8601String(),
'status': status.name,
'sessionId': sessionId,
'userId': userId,
'novelId': novelId,
'modelName': modelName,
'metadata': currentMetadata.isEmpty ? null : currentMetadata,
'sender': sender.name,
};
}
// 格式化时间戳
String get formattedTime => DateFormat('HH:mm').format(timestamp);
// 格式化日期
String get formattedDate => DateFormat('yyyy-MM-dd').format(timestamp);
}
// 消息发送者角色
enum MessageRole {
user,
assistant,
system,
}
// 消息状态
enum MessageStatus {
sending,
sent,
error,
pending,
delivered,
read,
streaming,
}
// 消息关联操作
class MessageAction {
MessageAction({
required this.id,
required this.label,
required this.type,
this.data,
});
// 从JSON转换方法
factory MessageAction.fromJson(Map<String, dynamic> json) {
return MessageAction(
id: json['id'] as String,
label: json['label'] as String,
type: ActionType.values.firstWhere(
(e) => e.toString() == 'ActionType.${json['type']}',
),
data: json['data'] as Map<String, dynamic>?,
);
}
final String id;
final String label;
final ActionType type;
final Map<String, dynamic>? data;
// 转换为JSON方法
Map<String, dynamic> toJson() {
return {
'id': id,
'label': label,
'type': type.toString().split('.').last,
'data': data,
};
}
}
// 操作类型
enum ActionType {
applyToEditor,
createCharacter,
createLocation,
generatePlot,
expandScene,
createChapter,
analyzeSentiment,
fixGrammar,
}
// 聊天上下文模型
class ChatContext {
ChatContext({
required this.novelId,
this.chapterId,
this.selectedText,
this.relevantItems = const [],
});
// 从JSON转换方法
factory ChatContext.fromJson(Map<String, dynamic> json) {
return ChatContext(
novelId: json['novelId'] as String,
chapterId: json['chapterId'] as String?,
selectedText: json['selectedText'] as String?,
relevantItems: json['relevantItems'] != null
? (json['relevantItems'] as List)
.map((e) => ContextItem.fromJson(e as Map<String, dynamic>))
.toList()
: [],
);
}
final String novelId;
final String? chapterId;
final String? selectedText;
final List<ContextItem> relevantItems;
// 复制方法
ChatContext copyWith({
String? novelId,
String? chapterId,
String? selectedText,
List<ContextItem>? relevantItems,
}) {
return ChatContext(
novelId: novelId ?? this.novelId,
chapterId: chapterId ?? this.chapterId,
selectedText: selectedText ?? this.selectedText,
relevantItems: relevantItems ?? this.relevantItems,
);
}
// 转换为JSON方法
Map<String, dynamic> toJson() {
return {
'novelId': novelId,
'chapterId': chapterId,
'selectedText': selectedText,
'relevantItems': relevantItems.map((e) => e.toJson()).toList(),
};
}
}
// 上下文项目
class ContextItem {
ContextItem({
required this.id,
required this.type,
required this.title,
required this.content,
required this.relevanceScore,
});
// 从JSON转换方法
factory ContextItem.fromJson(Map<String, dynamic> json) {
return ContextItem(
id: json['id'] as String,
type: ContextItemType.values.firstWhere(
(e) => e.toString() == 'ContextItemType.${json['type']}',
),
title: json['title'] as String,
content: json['content'] as String,
relevanceScore: json['relevanceScore'] as double,
);
}
final String id;
final ContextItemType type;
final String title;
final String content;
final double relevanceScore;
// 转换为JSON方法
Map<String, dynamic> toJson() {
return {
'id': id,
'type': type.toString().split('.').last,
'title': title,
'content': content,
'relevanceScore': relevanceScore,
};
}
}
// 上下文项目类型
enum ContextItemType {
character,
location,
plot,
chapter,
scene,
note,
lore,
}
// 消息发送者
enum MessageSender { user, ai }

View File

@@ -0,0 +1,41 @@
class ComposeChapterPreview {
final int index;
final String title;
final String outline;
final String content;
const ComposeChapterPreview({
required this.index,
this.title = '',
this.outline = '',
this.content = '',
});
ComposeChapterPreview copyWith({
String? title,
String? outline,
String? content,
}) {
return ComposeChapterPreview(
index: index,
title: title ?? this.title,
outline: outline ?? this.outline,
content: content ?? this.content,
);
}
}
class ComposeReadyInfo {
final bool ready;
final String reason;
final String novelId;
final String sessionId;
const ComposeReadyInfo({
required this.ready,
required this.reason,
required this.novelId,
required this.sessionId,
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,252 @@
/// 设定条目列表请求DTO
class SettingItemListRequest {
final String? type;
final String? name;
final int? priority;
final String? generatedBy;
final String? status;
final int page;
final int size;
final String sortBy;
final String sortDirection;
SettingItemListRequest({
this.type,
this.name,
this.priority,
this.generatedBy,
this.status,
this.page = 0,
this.size = 20,
this.sortBy = 'createdAt',
this.sortDirection = 'desc',
});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (type != null) data['type'] = type;
if (name != null) data['name'] = name;
if (priority != null) data['priority'] = priority;
if (generatedBy != null) data['generatedBy'] = generatedBy;
if (status != null) data['status'] = status;
data['page'] = page;
data['size'] = size;
data['sortBy'] = sortBy;
data['sortDirection'] = sortDirection;
return data;
}
}
/// 设定条目详情请求DTO
class SettingItemDetailRequest {
final String itemId;
SettingItemDetailRequest({required this.itemId});
Map<String, dynamic> toJson() {
return {
'itemId': itemId,
};
}
}
/// 设定条目更新请求DTO
class SettingItemUpdateRequest {
final String itemId;
final dynamic settingItem;
SettingItemUpdateRequest({required this.itemId, required this.settingItem});
Map<String, dynamic> toJson() {
return {
'itemId': itemId,
'settingItem': settingItem,
};
}
}
/// 设定条目删除请求DTO
class SettingItemDeleteRequest {
final String itemId;
SettingItemDeleteRequest({required this.itemId});
Map<String, dynamic> toJson() {
return {
'itemId': itemId,
};
}
}
/// 设定关系请求DTO
class SettingRelationshipRequest {
final String itemId;
final String targetItemId;
final String relationshipType;
final String? description;
SettingRelationshipRequest({
required this.itemId,
required this.targetItemId,
required this.relationshipType,
this.description,
});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['itemId'] = itemId;
data['targetItemId'] = targetItemId;
data['relationshipType'] = relationshipType;
if (description != null) data['description'] = description;
return data;
}
}
/// 设定关系删除请求DTO
class SettingRelationshipDeleteRequest {
final String itemId;
final String targetItemId;
final String relationshipType;
SettingRelationshipDeleteRequest({
required this.itemId,
required this.targetItemId,
required this.relationshipType,
});
Map<String, dynamic> toJson() {
return {
'itemId': itemId,
'targetItemId': targetItemId,
'relationshipType': relationshipType,
};
}
}
/// 设定组列表请求DTO
class SettingGroupListRequest {
final String? name;
final bool? isActiveContext;
SettingGroupListRequest({this.name, this.isActiveContext});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (name != null) data['name'] = name;
if (isActiveContext != null) data['isActiveContext'] = isActiveContext;
return data;
}
}
/// 设定组详情请求DTO
class SettingGroupDetailRequest {
final String groupId;
SettingGroupDetailRequest({required this.groupId});
Map<String, dynamic> toJson() {
return {
'groupId': groupId,
};
}
}
/// 设定组更新请求DTO
class SettingGroupUpdateRequest {
final String groupId;
final dynamic settingGroup;
SettingGroupUpdateRequest({required this.groupId, required this.settingGroup});
Map<String, dynamic> toJson() {
return {
'groupId': groupId,
'settingGroup': settingGroup,
};
}
}
/// 设定组删除请求DTO
class SettingGroupDeleteRequest {
final String groupId;
SettingGroupDeleteRequest({required this.groupId});
Map<String, dynamic> toJson() {
return {
'groupId': groupId,
};
}
}
/// 设定组条目请求DTO
class GroupItemRequest {
final String groupId;
final String itemId;
GroupItemRequest({required this.groupId, required this.itemId});
Map<String, dynamic> toJson() {
return {
'groupId': groupId,
'itemId': itemId,
};
}
}
/// 设置设定组激活状态请求DTO
class SetGroupActiveRequest {
final String groupId;
final bool active;
SetGroupActiveRequest({required this.groupId, required this.active});
Map<String, dynamic> toJson() {
return {
'groupId': groupId,
'active': active,
};
}
}
/// 从文本提取设定条目请求DTO
class ExtractSettingsRequest {
final String text;
final String type;
ExtractSettingsRequest({required this.text, required this.type});
Map<String, dynamic> toJson() {
return {
'text': text,
'type': type,
};
}
}
/// 搜索设定条目请求DTO
class SettingSearchRequest {
final String query;
final List<String>? types;
final List<String>? groupIds;
final double? minScore;
final int? maxResults;
SettingSearchRequest({
required this.query,
this.types,
this.groupIds,
this.minScore,
this.maxResults,
});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['query'] = query;
if (types != null) data['types'] = types;
if (groupIds != null) data['groupIds'] = groupIds;
if (minScore != null) data['minScore'] = minScore;
if (maxResults != null) data['maxResults'] = maxResults;
return data;
}
}

View File

@@ -0,0 +1,170 @@
import 'package:equatable/equatable.dart';
class EditorContent extends Equatable {
const EditorContent({
required this.id,
required this.content,
required this.lastSaved,
this.revisions = const [],
this.scenes,
});
// 从JSON转换
factory EditorContent.fromJson(Map<String, dynamic> json) {
Map<String, SceneContent>? scenesMap;
if (json['scenes'] != null) {
scenesMap = {};
json['scenes'].forEach((key, value) {
scenesMap![key] = SceneContent.fromJson(value);
});
}
return EditorContent(
id: json['id'],
content: json['content'],
lastSaved: DateTime.parse(json['lastSaved']),
revisions: (json['revisions'] as List?)
?.map((e) => Revision.fromJson(e))
.toList() ?? [],
scenes: scenesMap,
);
}
final String id;
final String content;
final DateTime lastSaved;
final List<Revision> revisions;
final Map<String, SceneContent>? scenes;
@override
List<Object?> get props => [id, content, lastSaved, revisions, scenes];
// 创建副本但更新部分内容
EditorContent copyWith({
String? id,
String? content,
DateTime? lastSaved,
List<Revision>? revisions,
Map<String, SceneContent>? scenes,
}) {
return EditorContent(
id: id ?? this.id,
content: content ?? this.content,
lastSaved: lastSaved ?? this.lastSaved,
revisions: revisions ?? this.revisions,
scenes: scenes ?? this.scenes,
);
}
// 转换为JSON
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = {
'id': id,
'content': content,
'lastSaved': lastSaved.toIso8601String(),
'revisions': revisions.map((e) => e.toJson()).toList(),
};
if (scenes != null) {
data['scenes'] = {};
scenes!.forEach((key, value) {
data['scenes'][key] = value.toJson();
});
}
return data;
}
}
class Revision extends Equatable {
const Revision({
required this.id,
required this.content,
required this.timestamp,
required this.authorId,
this.comment = '',
});
// 从JSON转换
factory Revision.fromJson(Map<String, dynamic> json) {
return Revision(
id: json['id'],
content: json['content'],
timestamp: DateTime.parse(json['timestamp']),
authorId: json['authorId'],
comment: json['comment'] ?? '',
);
}
final String id;
final String content;
final DateTime timestamp;
final String authorId;
final String comment;
@override
List<Object?> get props => [id, content, timestamp, authorId, comment];
// 转换为JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'content': content,
'timestamp': timestamp.toIso8601String(),
'authorId': authorId,
'comment': comment,
};
}
}
class SceneContent extends Equatable {
const SceneContent({
required this.content,
required this.summary,
required this.title,
required this.subtitle,
});
// 从JSON转换
factory SceneContent.fromJson(Map<String, dynamic> json) {
return SceneContent(
content: json['content'] ?? '',
summary: json['summary'] ?? '',
title: json['title'] ?? '',
subtitle: json['subtitle'] ?? '',
);
}
final String content;
final String summary;
final String title;
final String subtitle;
@override
List<Object?> get props => [content, summary, title, subtitle];
// 创建副本但更新部分内容
SceneContent copyWith({
String? content,
String? summary,
String? title,
String? subtitle,
}) {
return SceneContent(
content: content ?? this.content,
summary: summary ?? this.summary,
title: title ?? this.title,
subtitle: subtitle ?? this.subtitle,
);
}
// 转换为JSON
Map<String, dynamic> toJson() {
return {
'content': content,
'summary': summary,
'title': title,
'subtitle': subtitle,
};
}
}

View File

@@ -0,0 +1,371 @@
import 'package:flutter/material.dart';
/// 编辑器设置模型
/// 包含编辑器的所有可定制化选项
class EditorSettings {
const EditorSettings({
// 字体相关设置
this.fontSize = 16.0,
this.fontFamily = 'serif', // 🚀 改为中文友好的默认字体
this.fontWeight = FontWeight.normal,
this.lineSpacing = 1.5,
this.letterSpacing = 0.0, // 🚀 中文写作建议稍微调整字符间距
// 间距和布局设置
this.paddingHorizontal = 16.0,
this.paddingVertical = 12.0,
this.paragraphSpacing = 8.0,
this.indentSize = 32.0,
// 编辑器行为设置
this.autoSaveEnabled = true,
this.autoSaveIntervalMinutes = 5,
this.spellCheckEnabled = true,
this.showWordCount = true,
this.showLineNumbers = false,
this.highlightActiveLine = true,
// 主题和外观设置
this.darkModeEnabled = false,
this.showMiniMap = false,
this.smoothScrolling = true,
this.fadeInAnimation = true,
// 主题变体
this.themeVariant = 'monochrome',
// 编辑器宽度和高度设置
this.maxLineWidth = 1500.0,
this.minEditorHeight = 1200.0,
this.useTypewriterMode = false,
// 文本选择和光标设置
this.cursorBlinkRate = 1.0,
this.selectionHighlightColor = 0xFF2196F3,
this.enableVimMode = false,
// 导出和打印设置
this.defaultExportFormat = 'markdown',
this.includeMetadata = true,
});
// 字体相关设置
final double fontSize;
final String fontFamily;
final FontWeight fontWeight;
final double lineSpacing;
final double letterSpacing;
// 间距和布局设置
final double paddingHorizontal;
final double paddingVertical;
final double paragraphSpacing;
final double indentSize;
// 编辑器行为设置
final bool autoSaveEnabled;
final int autoSaveIntervalMinutes;
final bool spellCheckEnabled;
final bool showWordCount;
final bool showLineNumbers;
final bool highlightActiveLine;
// 主题和外观设置
final bool darkModeEnabled;
final bool showMiniMap;
final bool smoothScrolling;
final bool fadeInAnimation;
// 主题变体
final String themeVariant;
// 编辑器宽度和高度设置
final double maxLineWidth;
final double minEditorHeight;
final bool useTypewriterMode;
// 文本选择和光标设置
final double cursorBlinkRate;
final int selectionHighlightColor;
final bool enableVimMode;
// 导出和打印设置
final String defaultExportFormat;
final bool includeMetadata;
/// 复制并修改设置
EditorSettings copyWith({
double? fontSize,
String? fontFamily,
FontWeight? fontWeight,
double? lineSpacing,
double? letterSpacing,
double? paddingHorizontal,
double? paddingVertical,
double? paragraphSpacing,
double? indentSize,
bool? autoSaveEnabled,
int? autoSaveIntervalMinutes,
bool? spellCheckEnabled,
bool? showWordCount,
bool? showLineNumbers,
bool? highlightActiveLine,
bool? darkModeEnabled,
bool? showMiniMap,
bool? smoothScrolling,
bool? fadeInAnimation,
String? themeVariant,
double? maxLineWidth,
double? minEditorHeight,
bool? useTypewriterMode,
double? cursorBlinkRate,
int? selectionHighlightColor,
bool? enableVimMode,
String? defaultExportFormat,
bool? includeMetadata,
}) {
return EditorSettings(
fontSize: fontSize ?? this.fontSize,
fontFamily: fontFamily ?? this.fontFamily,
fontWeight: fontWeight ?? this.fontWeight,
lineSpacing: lineSpacing ?? this.lineSpacing,
letterSpacing: letterSpacing ?? this.letterSpacing,
paddingHorizontal: paddingHorizontal ?? this.paddingHorizontal,
paddingVertical: paddingVertical ?? this.paddingVertical,
paragraphSpacing: paragraphSpacing ?? this.paragraphSpacing,
indentSize: indentSize ?? this.indentSize,
autoSaveEnabled: autoSaveEnabled ?? this.autoSaveEnabled,
autoSaveIntervalMinutes: autoSaveIntervalMinutes ?? this.autoSaveIntervalMinutes,
spellCheckEnabled: spellCheckEnabled ?? this.spellCheckEnabled,
showWordCount: showWordCount ?? this.showWordCount,
showLineNumbers: showLineNumbers ?? this.showLineNumbers,
highlightActiveLine: highlightActiveLine ?? this.highlightActiveLine,
darkModeEnabled: darkModeEnabled ?? this.darkModeEnabled,
showMiniMap: showMiniMap ?? this.showMiniMap,
smoothScrolling: smoothScrolling ?? this.smoothScrolling,
fadeInAnimation: fadeInAnimation ?? this.fadeInAnimation,
themeVariant: themeVariant ?? this.themeVariant,
maxLineWidth: maxLineWidth ?? this.maxLineWidth,
minEditorHeight: minEditorHeight ?? this.minEditorHeight,
useTypewriterMode: useTypewriterMode ?? this.useTypewriterMode,
cursorBlinkRate: cursorBlinkRate ?? this.cursorBlinkRate,
selectionHighlightColor: selectionHighlightColor ?? this.selectionHighlightColor,
enableVimMode: enableVimMode ?? this.enableVimMode,
defaultExportFormat: defaultExportFormat ?? this.defaultExportFormat,
includeMetadata: includeMetadata ?? this.includeMetadata,
);
}
/// 转换为Map用于持久化存储
Map<String, dynamic> toMap() {
return {
'fontSize': fontSize,
'fontFamily': fontFamily,
'fontWeight': fontWeight.index,
'lineSpacing': lineSpacing,
'letterSpacing': letterSpacing,
'paddingHorizontal': paddingHorizontal,
'paddingVertical': paddingVertical,
'paragraphSpacing': paragraphSpacing,
'indentSize': indentSize,
'autoSaveEnabled': autoSaveEnabled,
'autoSaveIntervalMinutes': autoSaveIntervalMinutes,
'spellCheckEnabled': spellCheckEnabled,
'showWordCount': showWordCount,
'showLineNumbers': showLineNumbers,
'highlightActiveLine': highlightActiveLine,
'darkModeEnabled': darkModeEnabled,
'showMiniMap': showMiniMap,
'smoothScrolling': smoothScrolling,
'fadeInAnimation': fadeInAnimation,
'themeVariant': themeVariant,
'maxLineWidth': maxLineWidth,
'minEditorHeight': minEditorHeight,
'useTypewriterMode': useTypewriterMode,
'cursorBlinkRate': cursorBlinkRate,
'selectionHighlightColor': selectionHighlightColor,
'enableVimMode': enableVimMode,
'defaultExportFormat': defaultExportFormat,
'includeMetadata': includeMetadata,
};
}
/// 从Map创建用于持久化恢复
factory EditorSettings.fromMap(Map<String, dynamic> map) {
// 🚀 修复安全地转换fontWeight处理String和int类型
int fontWeightIndex = 3; // 默认值 FontWeight.normal
if (map['fontWeight'] != null) {
if (map['fontWeight'] is int) {
fontWeightIndex = map['fontWeight'];
} else if (map['fontWeight'] is String) {
fontWeightIndex = int.tryParse(map['fontWeight']) ?? 3;
}
}
// 🚀 修复安全地转换selectionHighlightColor处理String和int类型
int selectionColor = 0xFF2196F3; // 默认蓝色
if (map['selectionHighlightColor'] != null) {
if (map['selectionHighlightColor'] is int) {
selectionColor = map['selectionHighlightColor'];
} else if (map['selectionHighlightColor'] is String) {
selectionColor = int.tryParse(map['selectionHighlightColor']) ?? 0xFF2196F3;
}
}
// 🚀 修复安全地转换autoSaveIntervalMinutes处理String和int类型
int autoSaveInterval = 5; // 默认值
if (map['autoSaveIntervalMinutes'] != null) {
if (map['autoSaveIntervalMinutes'] is int) {
autoSaveInterval = map['autoSaveIntervalMinutes'];
} else if (map['autoSaveIntervalMinutes'] is String) {
autoSaveInterval = int.tryParse(map['autoSaveIntervalMinutes']) ?? 5;
}
}
return EditorSettings(
fontSize: map['fontSize']?.toDouble() ?? 16.0,
fontFamily: map['fontFamily'] ?? 'Roboto',
fontWeight: FontWeight.values[fontWeightIndex.clamp(0, FontWeight.values.length - 1)],
lineSpacing: map['lineSpacing']?.toDouble() ?? 1.5,
letterSpacing: map['letterSpacing']?.toDouble() ?? 0.0,
paddingHorizontal: map['paddingHorizontal']?.toDouble() ?? 16.0,
paddingVertical: map['paddingVertical']?.toDouble() ?? 12.0,
paragraphSpacing: map['paragraphSpacing']?.toDouble() ?? 8.0,
indentSize: map['indentSize']?.toDouble() ?? 32.0,
autoSaveEnabled: map['autoSaveEnabled'] ?? true,
autoSaveIntervalMinutes: autoSaveInterval,
spellCheckEnabled: map['spellCheckEnabled'] ?? true,
showWordCount: map['showWordCount'] ?? true,
showLineNumbers: map['showLineNumbers'] ?? false,
highlightActiveLine: map['highlightActiveLine'] ?? true,
darkModeEnabled: map['darkModeEnabled'] ?? false,
showMiniMap: map['showMiniMap'] ?? false,
smoothScrolling: map['smoothScrolling'] ?? true,
fadeInAnimation: map['fadeInAnimation'] ?? true,
themeVariant: (map['themeVariant'] as String?) ?? 'monochrome',
maxLineWidth: map['maxLineWidth']?.toDouble() ?? 1500.0,
minEditorHeight: map['minEditorHeight']?.toDouble() ?? 1200.0,
useTypewriterMode: map['useTypewriterMode'] ?? false,
cursorBlinkRate: map['cursorBlinkRate']?.toDouble() ?? 1.0,
selectionHighlightColor: selectionColor,
enableVimMode: map['enableVimMode'] ?? false,
defaultExportFormat: map['defaultExportFormat'] ?? 'markdown',
includeMetadata: map['includeMetadata'] ?? true,
);
}
/// 获取可用的字体列表
static List<String> get availableFontFamilies => [
'Roboto',
'serif', // 中文友好的衬线字体
'sans-serif', // 中文友好的无衬线字体
'monospace',
'Noto Sans SC', // Google Noto 简体中文字体
'PingFang SC', // 苹果中文字体
'Microsoft YaHei', // 微软雅黑
'SimHei', // 黑体
'SimSun', // 宋体
'Helvetica',
'Times New Roman',
'Courier New',
'Georgia',
'Verdana',
'Arial',
];
/// 获取可用的字体粗细选项
static List<FontWeight> get availableFontWeights => [
FontWeight.w300,
FontWeight.w400,
FontWeight.w500,
FontWeight.w600,
FontWeight.w700,
];
/// 获取可用的导出格式
static List<String> get availableExportFormats => [
'markdown',
'docx',
'pdf',
'txt',
'html',
];
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is EditorSettings &&
other.fontSize == fontSize &&
other.fontFamily == fontFamily &&
other.fontWeight == fontWeight &&
other.lineSpacing == lineSpacing &&
other.letterSpacing == letterSpacing &&
other.paddingHorizontal == paddingHorizontal &&
other.paddingVertical == paddingVertical &&
other.paragraphSpacing == paragraphSpacing &&
other.indentSize == indentSize &&
other.autoSaveEnabled == autoSaveEnabled &&
other.autoSaveIntervalMinutes == autoSaveIntervalMinutes &&
other.spellCheckEnabled == spellCheckEnabled &&
other.showWordCount == showWordCount &&
other.showLineNumbers == showLineNumbers &&
other.highlightActiveLine == highlightActiveLine &&
other.darkModeEnabled == darkModeEnabled &&
other.showMiniMap == showMiniMap &&
other.smoothScrolling == smoothScrolling &&
other.fadeInAnimation == fadeInAnimation &&
other.themeVariant == themeVariant &&
other.maxLineWidth == maxLineWidth &&
other.minEditorHeight == minEditorHeight &&
other.useTypewriterMode == useTypewriterMode &&
other.cursorBlinkRate == cursorBlinkRate &&
other.selectionHighlightColor == selectionHighlightColor &&
other.enableVimMode == enableVimMode &&
other.defaultExportFormat == defaultExportFormat &&
other.includeMetadata == includeMetadata;
}
@override
int get hashCode {
return Object.hashAll([
fontSize,
fontFamily,
fontWeight,
lineSpacing,
letterSpacing,
paddingHorizontal,
paddingVertical,
paragraphSpacing,
indentSize,
autoSaveEnabled,
autoSaveIntervalMinutes,
spellCheckEnabled,
showWordCount,
showLineNumbers,
highlightActiveLine,
darkModeEnabled,
showMiniMap,
smoothScrolling,
fadeInAnimation,
themeVariant,
maxLineWidth,
minEditorHeight,
useTypewriterMode,
cursorBlinkRate,
selectionHighlightColor,
enableVimMode,
defaultExportFormat,
includeMetadata,
]);
}
/// 🚀 新增转换为JSON用于API调用
Map<String, dynamic> toJson() {
return toMap();
}
/// 🚀 新增从JSON创建用于API响应
factory EditorSettings.fromJson(Map<String, dynamic> json) {
return EditorSettings.fromMap(json);
}
}

View File

@@ -0,0 +1,57 @@
/// 小说导入状态模型
class ImportStatus {
/// 从JSON创建实例
factory ImportStatus.fromJson(Map<String, dynamic> json) {
return ImportStatus(
status: json['status'] as String,
message: json['message'] as String,
progress: (json['progress'] as num?)?.toDouble(),
currentStep: json['currentStep'] as String?,
processedChapters: json['processedChapters'] as int?,
totalChapters: json['totalChapters'] as int?,
);
}
/// 创建导入状态
ImportStatus({
required this.status,
required this.message,
this.progress,
this.currentStep,
this.processedChapters,
this.totalChapters,
});
/// 导入状态 (PROCESSING, SAVING, INDEXING, COMPLETED, FAILED, ERROR)
final String status;
/// 状态消息
final String message;
/// 导入进度 (0.0 - 1.0)
final double? progress;
/// 当前步骤描述
final String? currentStep;
/// 已处理章节数
final int? processedChapters;
/// 总章节数
final int? totalChapters;
/// 转换为JSON
Map<String, dynamic> toJson() {
return {
'status': status,
'message': message,
if (progress != null) 'progress': progress,
if (currentStep != null) 'currentStep': currentStep,
if (processedChapters != null) 'processedChapters': processedChapters,
if (totalChapters != null) 'totalChapters': totalChapters,
};
}
@override
String toString() => 'ImportStatus{status: $status, message: $message, progress: $progress, currentStep: $currentStep, processedChapters: $processedChapters, totalChapters: $totalChapters}';
}

View File

@@ -0,0 +1,48 @@
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
/// Represents detailed information about an AI model provided by the backend.
@immutable
class ModelInfo extends Equatable {
final String id; // Usually the unique model identifier (e.g., "gpt-4o")
final String name; // User-friendly name (might be the same as id or different)
final String provider;
final String? description;
final int? maxTokens;
// Add other fields as needed based on backend response (e.g., pricing)
// final double? unifiedPrice;
const ModelInfo({
required this.id,
required this.name,
required this.provider,
this.description,
this.maxTokens,
// this.unifiedPrice,
});
factory ModelInfo.fromJson(Map<String, dynamic> json) {
return ModelInfo(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? json['id'] as String? ?? '', // Fallback name to id
provider: json['provider'] as String? ?? '',
description: json['description'] as String?,
maxTokens: json['maxTokens'] as int?,
// unifiedPrice: (json['unifiedPrice'] as num?)?.toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'provider': provider,
'description': description,
'maxTokens': maxTokens,
// 'unifiedPrice': unifiedPrice,
};
}
@override
List<Object?> get props => [id, name, provider, description, maxTokens /*, unifiedPrice*/];
}

View File

@@ -0,0 +1,334 @@
import 'package:json_annotation/json_annotation.dart';
/// 生成剧情大纲请求
class GenerateNextOutlinesRequest {
/// 上下文开始章节ID
final String? startChapterId;
/// 上下文结束章节ID
final String? endChapterId;
/// 生成选项数量
final int numOptions;
/// 作者引导
final String? authorGuidance;
/// 选定的AI模型配置ID列表
final List<String>? selectedConfigIds;
/// 重新生成提示(用于全局重新生成)
final String? regenerateHint;
GenerateNextOutlinesRequest({
this.startChapterId,
this.endChapterId,
this.numOptions = 3,
this.authorGuidance,
this.selectedConfigIds,
this.regenerateHint,
});
factory GenerateNextOutlinesRequest.fromJson(Map<String, dynamic> json) {
return GenerateNextOutlinesRequest(
startChapterId: json['startChapterId'] as String?,
endChapterId: json['endChapterId'] as String?,
numOptions: json['numOptions'] as int? ?? 3,
authorGuidance: json['authorGuidance'] as String?,
selectedConfigIds: (json['selectedConfigIds'] as List<dynamic>?)?.map((e) => e as String).toList(),
regenerateHint: json['regenerateHint'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
if (startChapterId != null) 'startChapterId': startChapterId,
if (endChapterId != null) 'endChapterId': endChapterId,
'numOptions': numOptions,
if (authorGuidance != null) 'authorGuidance': authorGuidance,
if (selectedConfigIds != null) 'selectedConfigIds': selectedConfigIds,
if (regenerateHint != null) 'regenerateHint': regenerateHint,
};
}
}
/// 生成剧情大纲响应
class GenerateNextOutlinesResponse {
/// 生成的大纲列表
final List<OutlineItem> outlines;
/// 生成时间(毫秒)
final int generationTimeMs;
GenerateNextOutlinesResponse({
required this.outlines,
required this.generationTimeMs,
});
factory GenerateNextOutlinesResponse.fromJson(Map<String, dynamic> json) {
return GenerateNextOutlinesResponse(
outlines: (json['outlines'] as List<dynamic>)
.map((e) => OutlineItem.fromJson(e as Map<String, dynamic>))
.toList(),
generationTimeMs: json['generationTimeMs'] as int,
);
}
Map<String, dynamic> toJson() {
return {
'outlines': outlines.map((e) => e.toJson()).toList(),
'generationTimeMs': generationTimeMs,
};
}
}
/// 大纲项
class OutlineItem {
/// 大纲ID
final String id;
/// 大纲标题
final String title;
/// 大纲内容
final String content;
/// 是否被选中
final bool isSelected;
/// 使用的模型配置ID
final String? configId;
OutlineItem({
required this.id,
required this.title,
required this.content,
required this.isSelected,
this.configId,
});
factory OutlineItem.fromJson(Map<String, dynamic> json) {
return OutlineItem(
id: json['id'] as String,
title: json['title'] as String,
content: json['content'] as String,
isSelected: json['isSelected'] as bool,
configId: json['configId'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'content': content,
'isSelected': isSelected,
if (configId != null) 'configId': configId,
};
}
}
/// 重新生成单个剧情大纲请求
class RegenerateOptionRequest {
/// 选项ID
final String optionId;
/// 选定的AI模型配置ID
final String selectedConfigId;
/// 重新生成提示
final String? regenerateHint;
RegenerateOptionRequest({
required this.optionId,
required this.selectedConfigId,
this.regenerateHint,
});
factory RegenerateOptionRequest.fromJson(Map<String, dynamic> json) {
return RegenerateOptionRequest(
optionId: json['optionId'] as String,
selectedConfigId: json['selectedConfigId'] as String,
regenerateHint: json['regenerateHint'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'optionId': optionId,
'selectedConfigId': selectedConfigId,
if (regenerateHint != null) 'regenerateHint': regenerateHint,
};
}
}
/// 保存剧情大纲请求
class SaveNextOutlineRequest {
/// 大纲ID
final String outlineId;
/// 插入位置类型
/// CHAPTER_END: 章节末尾
/// BEFORE_SCENE: 场景之前
/// AFTER_SCENE: 场景之后
/// NEW_CHAPTER: 新建章节(默认)
final String insertType;
/// 目标章节ID当insertType为CHAPTER_END时使用
final String? targetChapterId;
/// 目标场景ID当insertType为BEFORE_SCENE或AFTER_SCENE时使用
final String? targetSceneId;
/// 是否创建新场景默认为true
final bool createNewScene;
SaveNextOutlineRequest({
required this.outlineId,
this.insertType = 'NEW_CHAPTER',
this.targetChapterId,
this.targetSceneId,
this.createNewScene = true,
});
factory SaveNextOutlineRequest.fromJson(Map<String, dynamic> json) {
return SaveNextOutlineRequest(
outlineId: json['outlineId'] as String,
insertType: json['insertType'] as String? ?? 'NEW_CHAPTER',
targetChapterId: json['targetChapterId'] as String?,
targetSceneId: json['targetSceneId'] as String?,
createNewScene: json['createNewScene'] as bool? ?? true,
);
}
Map<String, dynamic> toJson() {
return {
'outlineId': outlineId,
'insertType': insertType,
if (targetChapterId != null) 'targetChapterId': targetChapterId,
if (targetSceneId != null) 'targetSceneId': targetSceneId,
'createNewScene': createNewScene,
};
}
}
/// 保存剧情大纲响应
class SaveNextOutlineResponse {
/// 是否成功
final bool success;
/// 保存的大纲ID
final String outlineId;
/// 新创建的章节ID如果有
final String? newChapterId;
/// 新创建的场景ID如果有
final String? newSceneId;
/// 目标章节ID如果指定了现有章节
final String? targetChapterId;
/// 目标场景ID如果指定了现有场景
final String? targetSceneId;
/// 插入位置类型
final String insertType;
/// 大纲标题(用于新章节标题)
final String outlineTitle;
SaveNextOutlineResponse({
required this.success,
required this.outlineId,
this.newChapterId,
this.newSceneId,
this.targetChapterId,
this.targetSceneId,
required this.insertType,
required this.outlineTitle,
});
factory SaveNextOutlineResponse.fromJson(Map<String, dynamic> json) {
return SaveNextOutlineResponse(
success: json['success'] as bool,
outlineId: json['outlineId'] as String,
newChapterId: json['newChapterId'] as String?,
newSceneId: json['newSceneId'] as String?,
targetChapterId: json['targetChapterId'] as String?,
targetSceneId: json['targetSceneId'] as String?,
insertType: json['insertType'] as String,
outlineTitle: json['outlineTitle'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'success': success,
'outlineId': outlineId,
if (newChapterId != null) 'newChapterId': newChapterId,
if (newSceneId != null) 'newSceneId': newSceneId,
if (targetChapterId != null) 'targetChapterId': targetChapterId,
if (targetSceneId != null) 'targetSceneId': targetSceneId,
'insertType': insertType,
'outlineTitle': outlineTitle,
};
}
}
/// 大纲生成输出结果
class NextOutlineOutput {
/// 大纲列表
final List<NextOutlineDTO> outlineList;
/// 生成时间(毫秒)
final int generationTimeMs;
/// 所选大纲索引
final int? selectedOutlineIndex;
NextOutlineOutput({
required this.outlineList,
required this.generationTimeMs,
this.selectedOutlineIndex,
});
Map<String, dynamic> toJson() {
return {
'outlineList': outlineList.map((e) => e.toJson()).toList(),
'generationTimeMs': generationTimeMs,
if (selectedOutlineIndex != null) 'selectedOutlineIndex': selectedOutlineIndex,
};
}
}
/// 剧情大纲DTO
class NextOutlineDTO {
/// 大纲ID
final String id;
/// 大纲标题
final String title;
/// 大纲内容
final String content;
/// 模型配置ID
final String? configId;
NextOutlineDTO({
required this.id,
required this.title,
required this.content,
this.configId,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'content': content,
if (configId != null) 'configId': configId,
};
}
}

View File

@@ -0,0 +1,48 @@
import 'package:json_annotation/json_annotation.dart';
/// 剧情大纲生成的数据块
/// 用于流式传输生成的剧情大纲选项
class OutlineGenerationChunk {
/// 选项ID用于唯一标识一个剧情选项
final String optionId;
/// 选项标题AI生成的剧情选项的短标题
final String? optionTitle;
/// 文本块内容,大纲内容的文本片段
final String textChunk;
/// 是否为该选项的最后一个块
final bool isFinalChunk;
/// 错误信息,如果生成过程中出错则包含错误信息
final String? error;
OutlineGenerationChunk({
required this.optionId,
this.optionTitle,
required this.textChunk,
required this.isFinalChunk,
this.error,
});
factory OutlineGenerationChunk.fromJson(Map<String, dynamic> json) {
return OutlineGenerationChunk(
optionId: json['optionId'] as String? ?? '',
optionTitle: json['optionTitle'] as String?,
textChunk: json['textChunk'] as String? ?? '',
isFinalChunk: json['finalChunk'] as bool? ?? false,
error: json['error'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'optionId': optionId,
if (optionTitle != null) 'optionTitle': optionTitle,
'textChunk': textChunk,
'isFinalChunk': isFinalChunk,
if (error != null) 'error': error,
};
}
}

View File

@@ -0,0 +1,291 @@
import 'dart:convert';
import 'package:equatable/equatable.dart';
import 'package:ainoval/models/ai_context_tracking.dart';
import 'package:ainoval/models/setting_relationship_type.dart';
/// 小说设定条目模型
class NovelSettingItem extends Equatable {
final String? id;
final String? novelId;
final String? userId;
final String name;
final String? type;
final String? content;
final String? description;
final Map<String, String>? attributes;
final String? imageUrl;
final List<SettingRelationship>? relationships;
final List<String>? sceneIds;
final int? priority;
final String? generatedBy;
final List<String>? tags;
final String? status;
final List<double>? vector;
final DateTime? createdAt;
final DateTime? updatedAt;
final bool isAiSuggestion;
final Map<String, dynamic>? metadata;
// ==================== 父子关系字段 ====================
/// 父设定ID建立层级关系的核心字段
final String? parentId;
/// 子设定ID列表冗余字段用于快速查询
final List<String>? childrenIds;
// ==================== AI上下文追踪字段 ====================
/// 名称/别名追踪设置
final NameAliasTracking nameAliasTracking;
/// AI上下文包含设置
final AIContextTracking aiContextTracking;
/// 设定引用更新设置
final SettingReferenceUpdate referenceUpdatePolicy;
const NovelSettingItem({
this.id,
this.novelId,
this.userId,
required this.name,
this.type,
this.content = "",
this.description,
this.attributes,
this.imageUrl,
this.relationships,
this.sceneIds,
this.priority,
this.generatedBy,
this.tags,
this.status,
this.vector,
this.createdAt,
this.updatedAt,
this.isAiSuggestion = false,
this.metadata,
this.parentId,
this.childrenIds,
this.nameAliasTracking = NameAliasTracking.track,
this.aiContextTracking = AIContextTracking.detected,
this.referenceUpdatePolicy = SettingReferenceUpdate.ask,
});
factory NovelSettingItem.fromJson(Map<String, dynamic> json) {
List<SettingRelationship>? relationships;
if (json['relationships'] != null && json['relationships'] is List) {
relationships = (json['relationships'] as List)
.map((e) => SettingRelationship.fromJson(e as Map<String, dynamic>))
.toList();
}
Map<String, String>? attributesMap;
if (json['attributes'] != null && json['attributes'] is Map) {
attributesMap = Map<String, String>.from(json['attributes'] as Map);
}
List<String>? tagsList;
if (json['tags'] != null && json['tags'] is List) {
tagsList = List<String>.from(json['tags'] as List);
}
List<String>? sceneIdsList;
if (json['sceneIds'] != null && json['sceneIds'] is List) {
sceneIdsList = List<String>.from(json['sceneIds'] as List);
}
List<String>? childrenIdsList;
if (json['childrenIds'] != null && json['childrenIds'] is List) {
childrenIdsList = List<String>.from(json['childrenIds'] as List);
}
List<double>? vectorList;
if (json['vector'] != null && json['vector'] is List) {
vectorList = (json['vector'] as List).map((e) => (e as num).toDouble()).toList();
}
Map<String, dynamic>? metadataMap;
if (json['metadata'] != null && json['metadata'] is Map) {
metadataMap = Map<String, dynamic>.from(json['metadata'] as Map);
}
return NovelSettingItem(
id: json['id'] as String?,
novelId: json['novelId'] as String?,
userId: json['userId'] as String?,
name: json['name'] as String? ?? '未命名设定',
type: json['type'] as String?,
content: json['content'] as String?,
description: json['description'] as String?,
attributes: attributesMap,
imageUrl: json['imageUrl'] as String?,
relationships: relationships,
sceneIds: sceneIdsList,
priority: json['priority'] as int?,
status: json['status'] as String?,
generatedBy: json['generatedBy'] as String?,
tags: tagsList,
vector: vectorList,
createdAt: json['createdAt'] != null ? DateTime.tryParse(json['createdAt'].toString()) : null,
updatedAt: json['updatedAt'] != null ? DateTime.tryParse(json['updatedAt'].toString()) : null,
isAiSuggestion: json['isAiSuggestion'] as bool? ?? false,
metadata: metadataMap,
parentId: json['parentId'] as String?,
childrenIds: childrenIdsList,
nameAliasTracking: NameAliasTracking.fromValue(json['nameAliasTracking'] as String?),
aiContextTracking: AIContextTracking.fromValue(json['aiContextTracking'] as String?),
referenceUpdatePolicy: SettingReferenceUpdate.fromValue(json['referenceUpdatePolicy'] as String?),
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (id != null) data['id'] = id;
if (novelId != null) data['novelId'] = novelId;
if (userId != null) data['userId'] = userId;
data['name'] = name;
if (type != null) data['type'] = type;
if (content != null) data['content'] = content;
if (description != null) data['description'] = description;
if (attributes != null) data['attributes'] = attributes;
if (imageUrl != null) data['imageUrl'] = imageUrl;
if (relationships != null) {
data['relationships'] = relationships!.map((e) => e.toJson()).toList();
}
if (sceneIds != null) data['sceneIds'] = sceneIds;
if (priority != null) data['priority'] = priority;
if (generatedBy != null) data['generatedBy'] = generatedBy;
if (tags != null) data['tags'] = tags;
if (status != null) data['status'] = status;
if (vector != null) data['vector'] = vector;
if (createdAt != null) data['createdAt'] = createdAt!.toIso8601String();
if (updatedAt != null) data['updatedAt'] = updatedAt!.toIso8601String();
data['isAiSuggestion'] = isAiSuggestion;
if (metadata != null) data['metadata'] = metadata;
if (parentId != null) data['parentId'] = parentId;
if (childrenIds != null) data['childrenIds'] = childrenIds;
data['nameAliasTracking'] = nameAliasTracking.value;
data['aiContextTracking'] = aiContextTracking.value;
data['referenceUpdatePolicy'] = referenceUpdatePolicy.value;
return data;
}
NovelSettingItem copyWith({
String? id,
String? novelId,
String? userId,
String? name,
String? type,
String? content,
String? description,
Map<String, String>? attributes,
String? imageUrl,
List<SettingRelationship>? relationships,
List<String>? sceneIds,
int? priority,
String? generatedBy,
List<String>? tags,
String? status,
List<double>? vector,
DateTime? createdAt,
DateTime? updatedAt,
bool? isAiSuggestion,
Map<String, dynamic>? metadata,
String? parentId,
List<String>? childrenIds,
NameAliasTracking? nameAliasTracking,
AIContextTracking? aiContextTracking,
SettingReferenceUpdate? referenceUpdatePolicy,
}) {
return NovelSettingItem(
id: id ?? this.id,
novelId: novelId ?? this.novelId,
userId: userId ?? this.userId,
name: name ?? this.name,
type: type ?? this.type,
content: content ?? this.content,
description: description ?? this.description,
attributes: attributes ?? this.attributes,
imageUrl: imageUrl ?? this.imageUrl,
relationships: relationships ?? this.relationships,
sceneIds: sceneIds ?? this.sceneIds,
priority: priority ?? this.priority,
generatedBy: generatedBy ?? this.generatedBy,
tags: tags ?? this.tags,
status: status ?? this.status,
vector: vector ?? this.vector,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
isAiSuggestion: isAiSuggestion ?? this.isAiSuggestion,
metadata: metadata ?? this.metadata,
parentId: parentId ?? this.parentId,
childrenIds: childrenIds ?? this.childrenIds,
nameAliasTracking: nameAliasTracking ?? this.nameAliasTracking,
aiContextTracking: aiContextTracking ?? this.aiContextTracking,
referenceUpdatePolicy: referenceUpdatePolicy ?? this.referenceUpdatePolicy,
);
}
@override
List<Object?> get props => [
id, novelId, userId, name, type, content, description, attributes,
imageUrl, relationships, sceneIds, priority, generatedBy, tags, status,
vector, createdAt, updatedAt, isAiSuggestion, metadata, parentId, childrenIds,
nameAliasTracking, aiContextTracking, referenceUpdatePolicy
];
@override
String toString() {
return jsonEncode(toJson());
}
}
/// 设定关系模型
class SettingRelationship extends Equatable {
final String targetItemId;
final SettingRelationshipType type;
final String? description;
final int? strength;
final String? direction;
final DateTime? createdAt;
final Map<String, dynamic>? attributes;
const SettingRelationship({
required this.targetItemId,
required this.type,
this.description,
this.strength,
this.direction,
this.createdAt,
this.attributes,
});
factory SettingRelationship.fromJson(Map<String, dynamic> json) {
return SettingRelationship(
targetItemId: json['targetItemId'] as String,
type: SettingRelationshipType.fromValue(json['type'] as String),
description: json['description'] as String?,
strength: json['strength'] as int?,
direction: json['direction'] as String?,
createdAt: json['createdAt'] != null ? DateTime.tryParse(json['createdAt'].toString()) : null,
attributes: json['attributes'] != null ? Map<String, dynamic>.from(json['attributes']) : null,
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['targetItemId'] = targetItemId;
data['type'] = type.value;
if (description != null) data['description'] = description;
if (strength != null) data['strength'] = strength;
if (direction != null) data['direction'] = direction;
if (createdAt != null) data['createdAt'] = createdAt!.toIso8601String();
if (attributes != null) data['attributes'] = attributes;
return data;
}
@override
List<Object?> get props => [targetItemId, type, description, strength, direction, createdAt, attributes];
}

View File

@@ -0,0 +1,312 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:ainoval/utils/date_time_parser.dart';
part 'novel_snippet.g.dart';
/// 小说片段模型
@JsonSerializable()
class NovelSnippet {
final String id;
final String userId;
final String novelId;
final String title;
final String content;
final InitialGenerationInfo? initialGenerationInfo;
final List<String>? tags;
final String? category;
final String? notes;
final SnippetMetadata metadata;
final bool isFavorite;
final String status;
final int version;
@JsonKey(fromJson: parseBackendDateTime, toJson: _dateTimeToJson)
final DateTime createdAt;
@JsonKey(fromJson: parseBackendDateTime, toJson: _dateTimeToJson)
final DateTime updatedAt;
const NovelSnippet({
required this.id,
required this.userId,
required this.novelId,
required this.title,
required this.content,
this.initialGenerationInfo,
this.tags,
this.category,
this.notes,
required this.metadata,
required this.isFavorite,
required this.status,
required this.version,
required this.createdAt,
required this.updatedAt,
});
factory NovelSnippet.fromJson(Map<String, dynamic> json) =>
_$NovelSnippetFromJson(json);
Map<String, dynamic> toJson() => _$NovelSnippetToJson(this);
static String _dateTimeToJson(DateTime dateTime) => dateTime.toIso8601String();
NovelSnippet copyWith({
String? id,
String? userId,
String? novelId,
String? title,
String? content,
InitialGenerationInfo? initialGenerationInfo,
List<String>? tags,
String? category,
String? notes,
SnippetMetadata? metadata,
bool? isFavorite,
String? status,
int? version,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return NovelSnippet(
id: id ?? this.id,
userId: userId ?? this.userId,
novelId: novelId ?? this.novelId,
title: title ?? this.title,
content: content ?? this.content,
initialGenerationInfo: initialGenerationInfo ?? this.initialGenerationInfo,
tags: tags ?? this.tags,
category: category ?? this.category,
notes: notes ?? this.notes,
metadata: metadata ?? this.metadata,
isFavorite: isFavorite ?? this.isFavorite,
status: status ?? this.status,
version: version ?? this.version,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}
/// 初始生成信息
@JsonSerializable()
class InitialGenerationInfo {
final String? sourceChapterId;
final String? sourceSceneId;
const InitialGenerationInfo({
this.sourceChapterId,
this.sourceSceneId,
});
factory InitialGenerationInfo.fromJson(Map<String, dynamic> json) =>
_$InitialGenerationInfoFromJson(json);
Map<String, dynamic> toJson() => _$InitialGenerationInfoToJson(this);
}
/// 片段元数据
@JsonSerializable()
class SnippetMetadata {
final int wordCount;
final int characterCount;
final int viewCount;
final int sortWeight;
@JsonKey(fromJson: _parseOptionalDateTime, toJson: _optionalDateTimeToJson)
final DateTime? lastViewedAt;
const SnippetMetadata({
required this.wordCount,
required this.characterCount,
required this.viewCount,
required this.sortWeight,
this.lastViewedAt,
});
factory SnippetMetadata.fromJson(Map<String, dynamic> json) =>
_$SnippetMetadataFromJson(json);
Map<String, dynamic> toJson() => _$SnippetMetadataToJson(this);
static DateTime? _parseOptionalDateTime(dynamic value) {
return value == null ? null : parseBackendDateTime(value);
}
static String? _optionalDateTimeToJson(DateTime? dateTime) {
return dateTime?.toIso8601String();
}
}
/// 小说片段历史记录
@JsonSerializable()
class NovelSnippetHistory {
final String id;
final String snippetId;
final String userId;
final String operationType;
final int version;
final String? beforeTitle;
final String? afterTitle;
final String? beforeContent;
final String? afterContent;
final String? changeDescription;
@JsonKey(fromJson: parseBackendDateTime, toJson: _dateTimeToJson)
final DateTime createdAt;
const NovelSnippetHistory({
required this.id,
required this.snippetId,
required this.userId,
required this.operationType,
required this.version,
this.beforeTitle,
this.afterTitle,
this.beforeContent,
this.afterContent,
this.changeDescription,
required this.createdAt,
});
factory NovelSnippetHistory.fromJson(Map<String, dynamic> json) =>
_$NovelSnippetHistoryFromJson(json);
Map<String, dynamic> toJson() => _$NovelSnippetHistoryToJson(this);
static String _dateTimeToJson(DateTime dateTime) => dateTime.toIso8601String();
}
/// 分页结果包装类
@JsonSerializable(genericArgumentFactories: true)
class SnippetPageResult<T> {
final List<T> content;
final int page;
final int size;
final int totalElements;
final int totalPages;
final bool hasNext;
final bool hasPrevious;
const SnippetPageResult({
required this.content,
required this.page,
required this.size,
required this.totalElements,
required this.totalPages,
required this.hasNext,
required this.hasPrevious,
});
factory SnippetPageResult.fromJson(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) =>
_$SnippetPageResultFromJson(json, fromJsonT);
Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$SnippetPageResultToJson(this, toJsonT);
}
/// 创建片段请求
@JsonSerializable()
class CreateSnippetRequest {
final String novelId;
final String title;
final String content;
final String? sourceChapterId;
final String? sourceSceneId;
final List<String>? tags;
final String? category;
final String? notes;
const CreateSnippetRequest({
required this.novelId,
required this.title,
required this.content,
this.sourceChapterId,
this.sourceSceneId,
this.tags,
this.category,
this.notes,
});
factory CreateSnippetRequest.fromJson(Map<String, dynamic> json) =>
_$CreateSnippetRequestFromJson(json);
Map<String, dynamic> toJson() => _$CreateSnippetRequestToJson(this);
}
/// 更新片段内容请求
@JsonSerializable()
class UpdateSnippetContentRequest {
final String snippetId;
final String content;
final String? changeDescription;
const UpdateSnippetContentRequest({
required this.snippetId,
required this.content,
this.changeDescription,
});
factory UpdateSnippetContentRequest.fromJson(Map<String, dynamic> json) =>
_$UpdateSnippetContentRequestFromJson(json);
Map<String, dynamic> toJson() => _$UpdateSnippetContentRequestToJson(this);
}
/// 更新片段标题请求
@JsonSerializable()
class UpdateSnippetTitleRequest {
final String snippetId;
final String title;
final String? changeDescription;
const UpdateSnippetTitleRequest({
required this.snippetId,
required this.title,
this.changeDescription,
});
factory UpdateSnippetTitleRequest.fromJson(Map<String, dynamic> json) =>
_$UpdateSnippetTitleRequestFromJson(json);
Map<String, dynamic> toJson() => _$UpdateSnippetTitleRequestToJson(this);
}
/// 更新收藏状态请求
@JsonSerializable()
class UpdateSnippetFavoriteRequest {
final String snippetId;
final bool isFavorite;
const UpdateSnippetFavoriteRequest({
required this.snippetId,
required this.isFavorite,
});
factory UpdateSnippetFavoriteRequest.fromJson(Map<String, dynamic> json) =>
_$UpdateSnippetFavoriteRequestFromJson(json);
Map<String, dynamic> toJson() => _$UpdateSnippetFavoriteRequestToJson(this);
}
/// 回退版本请求
@JsonSerializable()
class RevertSnippetVersionRequest {
final String snippetId;
final int version;
final String? changeDescription;
const RevertSnippetVersionRequest({
required this.snippetId,
required this.version,
this.changeDescription,
});
factory RevertSnippetVersionRequest.fromJson(Map<String, dynamic> json) =>
_$RevertSnippetVersionRequestFromJson(json);
Map<String, dynamic> toJson() => _$RevertSnippetVersionRequestToJson(this);
}

View File

@@ -0,0 +1,386 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'novel_snippet.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
NovelSnippet _$NovelSnippetFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'NovelSnippet',
json,
($checkedConvert) {
final val = NovelSnippet(
id: $checkedConvert('id', (v) => v as String),
userId: $checkedConvert('userId', (v) => v as String),
novelId: $checkedConvert('novelId', (v) => v as String),
title: $checkedConvert('title', (v) => v as String),
content: $checkedConvert('content', (v) => v as String),
initialGenerationInfo: $checkedConvert(
'initialGenerationInfo',
(v) => v == null
? null
: InitialGenerationInfo.fromJson(v as Map<String, dynamic>)),
tags: $checkedConvert('tags',
(v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
category: $checkedConvert('category', (v) => v as String?),
notes: $checkedConvert('notes', (v) => v as String?),
metadata: $checkedConvert('metadata',
(v) => SnippetMetadata.fromJson(v as Map<String, dynamic>)),
isFavorite: $checkedConvert('isFavorite', (v) => v as bool),
status: $checkedConvert('status', (v) => v as String),
version: $checkedConvert('version', (v) => (v as num).toInt()),
createdAt:
$checkedConvert('createdAt', (v) => parseBackendDateTime(v)),
updatedAt:
$checkedConvert('updatedAt', (v) => parseBackendDateTime(v)),
);
return val;
},
);
Map<String, dynamic> _$NovelSnippetToJson(NovelSnippet instance) {
final val = <String, dynamic>{
'id': instance.id,
'userId': instance.userId,
'novelId': instance.novelId,
'title': instance.title,
'content': instance.content,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull(
'initialGenerationInfo', instance.initialGenerationInfo?.toJson());
writeNotNull('tags', instance.tags);
writeNotNull('category', instance.category);
writeNotNull('notes', instance.notes);
val['metadata'] = instance.metadata.toJson();
val['isFavorite'] = instance.isFavorite;
val['status'] = instance.status;
val['version'] = instance.version;
val['createdAt'] = NovelSnippet._dateTimeToJson(instance.createdAt);
val['updatedAt'] = NovelSnippet._dateTimeToJson(instance.updatedAt);
return val;
}
InitialGenerationInfo _$InitialGenerationInfoFromJson(
Map<String, dynamic> json) =>
$checkedCreate(
'InitialGenerationInfo',
json,
($checkedConvert) {
final val = InitialGenerationInfo(
sourceChapterId:
$checkedConvert('sourceChapterId', (v) => v as String?),
sourceSceneId: $checkedConvert('sourceSceneId', (v) => v as String?),
);
return val;
},
);
Map<String, dynamic> _$InitialGenerationInfoToJson(
InitialGenerationInfo instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('sourceChapterId', instance.sourceChapterId);
writeNotNull('sourceSceneId', instance.sourceSceneId);
return val;
}
SnippetMetadata _$SnippetMetadataFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'SnippetMetadata',
json,
($checkedConvert) {
final val = SnippetMetadata(
wordCount: $checkedConvert('wordCount', (v) => (v as num).toInt()),
characterCount:
$checkedConvert('characterCount', (v) => (v as num).toInt()),
viewCount: $checkedConvert('viewCount', (v) => (v as num).toInt()),
sortWeight: $checkedConvert('sortWeight', (v) => (v as num).toInt()),
lastViewedAt: $checkedConvert(
'lastViewedAt', (v) => SnippetMetadata._parseOptionalDateTime(v)),
);
return val;
},
);
Map<String, dynamic> _$SnippetMetadataToJson(SnippetMetadata instance) {
final val = <String, dynamic>{
'wordCount': instance.wordCount,
'characterCount': instance.characterCount,
'viewCount': instance.viewCount,
'sortWeight': instance.sortWeight,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('lastViewedAt',
SnippetMetadata._optionalDateTimeToJson(instance.lastViewedAt));
return val;
}
NovelSnippetHistory _$NovelSnippetHistoryFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'NovelSnippetHistory',
json,
($checkedConvert) {
final val = NovelSnippetHistory(
id: $checkedConvert('id', (v) => v as String),
snippetId: $checkedConvert('snippetId', (v) => v as String),
userId: $checkedConvert('userId', (v) => v as String),
operationType: $checkedConvert('operationType', (v) => v as String),
version: $checkedConvert('version', (v) => (v as num).toInt()),
beforeTitle: $checkedConvert('beforeTitle', (v) => v as String?),
afterTitle: $checkedConvert('afterTitle', (v) => v as String?),
beforeContent: $checkedConvert('beforeContent', (v) => v as String?),
afterContent: $checkedConvert('afterContent', (v) => v as String?),
changeDescription:
$checkedConvert('changeDescription', (v) => v as String?),
createdAt:
$checkedConvert('createdAt', (v) => parseBackendDateTime(v)),
);
return val;
},
);
Map<String, dynamic> _$NovelSnippetHistoryToJson(NovelSnippetHistory instance) {
final val = <String, dynamic>{
'id': instance.id,
'snippetId': instance.snippetId,
'userId': instance.userId,
'operationType': instance.operationType,
'version': instance.version,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('beforeTitle', instance.beforeTitle);
writeNotNull('afterTitle', instance.afterTitle);
writeNotNull('beforeContent', instance.beforeContent);
writeNotNull('afterContent', instance.afterContent);
writeNotNull('changeDescription', instance.changeDescription);
val['createdAt'] = NovelSnippetHistory._dateTimeToJson(instance.createdAt);
return val;
}
SnippetPageResult<T> _$SnippetPageResultFromJson<T>(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) =>
$checkedCreate(
'SnippetPageResult',
json,
($checkedConvert) {
final val = SnippetPageResult<T>(
content: $checkedConvert(
'content', (v) => (v as List<dynamic>).map(fromJsonT).toList()),
page: $checkedConvert('page', (v) => (v as num).toInt()),
size: $checkedConvert('size', (v) => (v as num).toInt()),
totalElements:
$checkedConvert('totalElements', (v) => (v as num).toInt()),
totalPages: $checkedConvert('totalPages', (v) => (v as num).toInt()),
hasNext: $checkedConvert('hasNext', (v) => v as bool),
hasPrevious: $checkedConvert('hasPrevious', (v) => v as bool),
);
return val;
},
);
Map<String, dynamic> _$SnippetPageResultToJson<T>(
SnippetPageResult<T> instance,
Object? Function(T value) toJsonT,
) =>
<String, dynamic>{
'content': instance.content.map(toJsonT).toList(),
'page': instance.page,
'size': instance.size,
'totalElements': instance.totalElements,
'totalPages': instance.totalPages,
'hasNext': instance.hasNext,
'hasPrevious': instance.hasPrevious,
};
CreateSnippetRequest _$CreateSnippetRequestFromJson(
Map<String, dynamic> json) =>
$checkedCreate(
'CreateSnippetRequest',
json,
($checkedConvert) {
final val = CreateSnippetRequest(
novelId: $checkedConvert('novelId', (v) => v as String),
title: $checkedConvert('title', (v) => v as String),
content: $checkedConvert('content', (v) => v as String),
sourceChapterId:
$checkedConvert('sourceChapterId', (v) => v as String?),
sourceSceneId: $checkedConvert('sourceSceneId', (v) => v as String?),
tags: $checkedConvert('tags',
(v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
category: $checkedConvert('category', (v) => v as String?),
notes: $checkedConvert('notes', (v) => v as String?),
);
return val;
},
);
Map<String, dynamic> _$CreateSnippetRequestToJson(
CreateSnippetRequest instance) {
final val = <String, dynamic>{
'novelId': instance.novelId,
'title': instance.title,
'content': instance.content,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('sourceChapterId', instance.sourceChapterId);
writeNotNull('sourceSceneId', instance.sourceSceneId);
writeNotNull('tags', instance.tags);
writeNotNull('category', instance.category);
writeNotNull('notes', instance.notes);
return val;
}
UpdateSnippetContentRequest _$UpdateSnippetContentRequestFromJson(
Map<String, dynamic> json) =>
$checkedCreate(
'UpdateSnippetContentRequest',
json,
($checkedConvert) {
final val = UpdateSnippetContentRequest(
snippetId: $checkedConvert('snippetId', (v) => v as String),
content: $checkedConvert('content', (v) => v as String),
changeDescription:
$checkedConvert('changeDescription', (v) => v as String?),
);
return val;
},
);
Map<String, dynamic> _$UpdateSnippetContentRequestToJson(
UpdateSnippetContentRequest instance) {
final val = <String, dynamic>{
'snippetId': instance.snippetId,
'content': instance.content,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('changeDescription', instance.changeDescription);
return val;
}
UpdateSnippetTitleRequest _$UpdateSnippetTitleRequestFromJson(
Map<String, dynamic> json) =>
$checkedCreate(
'UpdateSnippetTitleRequest',
json,
($checkedConvert) {
final val = UpdateSnippetTitleRequest(
snippetId: $checkedConvert('snippetId', (v) => v as String),
title: $checkedConvert('title', (v) => v as String),
changeDescription:
$checkedConvert('changeDescription', (v) => v as String?),
);
return val;
},
);
Map<String, dynamic> _$UpdateSnippetTitleRequestToJson(
UpdateSnippetTitleRequest instance) {
final val = <String, dynamic>{
'snippetId': instance.snippetId,
'title': instance.title,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('changeDescription', instance.changeDescription);
return val;
}
UpdateSnippetFavoriteRequest _$UpdateSnippetFavoriteRequestFromJson(
Map<String, dynamic> json) =>
$checkedCreate(
'UpdateSnippetFavoriteRequest',
json,
($checkedConvert) {
final val = UpdateSnippetFavoriteRequest(
snippetId: $checkedConvert('snippetId', (v) => v as String),
isFavorite: $checkedConvert('isFavorite', (v) => v as bool),
);
return val;
},
);
Map<String, dynamic> _$UpdateSnippetFavoriteRequestToJson(
UpdateSnippetFavoriteRequest instance) =>
<String, dynamic>{
'snippetId': instance.snippetId,
'isFavorite': instance.isFavorite,
};
RevertSnippetVersionRequest _$RevertSnippetVersionRequestFromJson(
Map<String, dynamic> json) =>
$checkedCreate(
'RevertSnippetVersionRequest',
json,
($checkedConvert) {
final val = RevertSnippetVersionRequest(
snippetId: $checkedConvert('snippetId', (v) => v as String),
version: $checkedConvert('version', (v) => (v as num).toInt()),
changeDescription:
$checkedConvert('changeDescription', (v) => v as String?),
);
return val;
},
);
Map<String, dynamic> _$RevertSnippetVersionRequestToJson(
RevertSnippetVersionRequest instance) {
final val = <String, dynamic>{
'snippetId': instance.snippetId,
'version': instance.version,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('changeDescription', instance.changeDescription);
return val;
}

View File

@@ -0,0 +1,950 @@
import 'package:ainoval/utils/logger.dart';
/// 小说模型
class Novel {
Novel({
required this.id,
required this.title,
this.coverUrl= '',
required this.createdAt,
required this.updatedAt,
this.acts = const [],
this.lastEditedChapterId,
this.author,
this.wordCount = 0,
this.readTime = 0,
this.version = 1,
this.contributors = const <String>[],
});
/// 从JSON创建Novel实例
factory Novel.fromJson(Map<String, dynamic> json) {
AppLogger.v(
'NovelModel', 'Parsing Novel from JSON: ${json['id']}'); // 添加日志确认进入
try {
// --- 这是关键部分 ---
List<Act> parsedActs = [];
// 处理acts数据 - 优先检查structure.acts路径
if (json.containsKey('structure') && json['structure'] is Map) {
final structure = json['structure'] as Map<String, dynamic>;
if (structure.containsKey('acts') && structure['acts'] is List) {
AppLogger.v('NovelModel',
'Found "structure.acts" list with ${(structure['acts'] as List).length} items.');
parsedActs = (structure['acts'] as List)
.map((actJson) {
if (actJson is Map<String, dynamic>) {
// 对列表中的每个元素调用 Act.fromJson
return Act.fromJson(actJson);
} else {
// 处理无效数据项
AppLogger.w('NovelModel',
'Invalid item in "structure.acts" list: $actJson');
return null; // 返回null让whereType过滤掉
}
})
.whereType<Act>() // 过滤掉可能的 null 值
.toList();
AppLogger.v('NovelModel',
'Successfully parsed ${parsedActs.length} acts from structure.acts.');
} else {
AppLogger.w('NovelModel',
'"structure.acts" field is missing, null, or not a list in JSON for Novel ${json['id']}');
}
}
// 如果在structure中没有找到有效的acts尝试直接从json的acts字段读取
else if (json.containsKey('acts') && json['acts'] is List) {
AppLogger.v('NovelModel',
'Found direct "acts" list with ${(json['acts'] as List).length} items.');
parsedActs = (json['acts'] as List)
.map((actJson) {
if (actJson is Map<String, dynamic>) {
return Act.fromJson(actJson);
} else {
AppLogger.w('NovelModel',
'Invalid item in direct "acts" list: $actJson');
return null;
}
})
.whereType<Act>()
.toList();
AppLogger.v('NovelModel',
'Successfully parsed ${parsedActs.length} acts from direct acts field.');
} else {
AppLogger.w('NovelModel',
'No valid acts field found in JSON for Novel ${json['id']}');
}
// --- 关键部分结束 ---
// 解析元数据
final metadata = json['metadata'] as Map<String, dynamic>? ?? {};
final wordCount = metadata['wordCount'] is int ? metadata['wordCount'] as int : 0;
final readTime = metadata['readTime'] is int ? metadata['readTime'] as int : 0;
final version = metadata['version'] is int ? metadata['version'] as int : 1;
// 处理contributors列表
List<String> contributors = [];
if (metadata.containsKey('contributors') && metadata['contributors'] is List) {
// 尝试转换每个元素为String
for (var item in metadata['contributors'] as List) {
if (item is String) {
contributors.add(item);
}
}
}
// 解析日期
DateTime createdAt;
DateTime updatedAt;
try {
createdAt = json.containsKey('createdAt') && json['createdAt'] is String
? DateTime.parse(json['createdAt'] as String)
: DateTime.now();
} catch (e) {
AppLogger.w('NovelModel', '解析createdAt失败使用当前时间', e);
createdAt = DateTime.now();
}
try {
updatedAt = json.containsKey('updatedAt') && json['updatedAt'] is String
? DateTime.parse(json['updatedAt'] as String)
: DateTime.now();
} catch (e) {
AppLogger.w('NovelModel', '解析updatedAt失败使用当前时间', e);
updatedAt = DateTime.now();
}
// 处理封面URL字段
String coverUrl = '';
if (json.containsKey('coverUrl') && json['coverUrl'] is String) {
coverUrl = json['coverUrl'] as String;
} else if (json.containsKey('coverImage') && json['coverImage'] is String) {
// 兼容后端可能使用coverImage字段
coverUrl = json['coverImage'] as String;
}
// 创建Novel对象
return Novel(
id: json['id'] as String? ?? 'unknown_${DateTime.now().millisecondsSinceEpoch}',
title: json['title'] as String? ?? '无标题',
coverUrl: coverUrl,
createdAt: createdAt,
updatedAt: updatedAt,
acts: parsedActs,
lastEditedChapterId: json['lastEditedChapterId'] as String?,
author: json['author'] != null
? Author.fromJson(json['author'] as Map<String, dynamic>)
: null,
wordCount: wordCount,
readTime: readTime,
version: version,
contributors: contributors,
);
} catch (e, stackTrace) {
AppLogger.e('NovelModel', 'Error parsing Novel from JSON: ${json['id']}',
e, stackTrace);
// 返回一个基本的空Novel对象避免应用崩溃
return Novel(
id: json['id'] as String? ?? 'error_${DateTime.now().millisecondsSinceEpoch}',
title: '解析错误 - ${json['title'] ?? '无标题'}',
coverUrl: '',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
acts: [],
wordCount: 0,
);
}
}
final String id;
final String title;
final String coverUrl;
final DateTime createdAt;
final DateTime updatedAt;
final List<Act> acts;
final String? lastEditedChapterId; // 上次编辑的章节ID
final Author? author; // 作者信息
final int wordCount; // 总字数(来自元数据)
final int readTime; // 估计阅读时间(分钟)
final int version; // 文档版本号
final List<String> contributors; // 贡献者列表
/// 计算小说总字数(如果需要动态计算)
int calculateWordCount() {
int totalWordCount = 0;
for (final act in acts) {
for (final chapter in act.chapters) {
for (final scene in chapter.scenes) {
totalWordCount += scene.wordCount;
}
}
}
return totalWordCount;
}
/// 计算小说总场景数(考虑 sceneIds 字段)
int getSceneCount() {
int totalSceneCount = 0;
//AppLogger.d('Novel', '开始计算场景总数');
for (final act in acts) {
int actSceneCount = 0;
for (final chapter in act.chapters) {
// 使用 sceneCount 属性,它会返回 scenes 和 sceneIds 中的较大值
int chapterSceneCount = chapter.sceneCount;
actSceneCount += chapterSceneCount;
//AppLogger.d('Novel', '章节 ${chapter.id} 场景数: scenes=${chapter.scenes.length}, sceneIds=${chapter.sceneIds.length}, 取较大值=${chapterSceneCount}');
}
totalSceneCount += actSceneCount;
//AppLogger.d('Novel', '卷 ${act.id} 场景总数: $actSceneCount');
}
//AppLogger.d('Novel', '小说场景总数: $totalSceneCount');
return totalSceneCount;
}
/// 计算小说总章节数
int getChapterCount() {
int totalChapterCount = 0;
for (final act in acts) {
totalChapterCount += act.chapters.length;
}
return totalChapterCount;
}
/// 计算小说总卷数
int getActCount() {
return acts.length;
}
/// 转换为JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'coverUrl': coverUrl,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'acts': acts.map((act) => act.toJson()).toList(),
'lastEditedChapterId': lastEditedChapterId,
'author': author?.toJson(),
'metadata': {
'wordCount': wordCount,
'readTime': readTime,
'version': version,
'contributors': contributors,
},
};
}
/// 创建Novel的副本
Novel copyWith({
String? id,
String? title,
String? coverUrl,
DateTime? createdAt,
DateTime? updatedAt,
List<Act>? acts,
String? lastEditedChapterId,
Author? author,
int? wordCount,
int? readTime,
int? version,
List<String>? contributors,
}) {
return Novel(
id: id ?? this.id,
title: title ?? this.title,
coverUrl: coverUrl?? this.coverUrl,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
acts: acts ?? this.acts,
lastEditedChapterId: lastEditedChapterId ?? this.lastEditedChapterId,
author: author ?? this.author,
wordCount: wordCount ?? this.wordCount,
readTime: readTime ?? this.readTime,
version: version ?? this.version,
contributors: contributors ?? this.contributors,
);
}
/// 创建一个空的小说结构
static Novel createEmpty(String id, String title) {
final now = DateTime.now();
return Novel(
id: id,
title: title,
createdAt: now,
updatedAt: now,
acts: [],
);
}
/// 添加一个新的Act
Novel addAct(String title) {
final newAct = Act(
id: 'act_${DateTime.now().millisecondsSinceEpoch}',
title: title,
order: acts.length + 1,
chapters: [],
);
return copyWith(
acts: [...acts, newAct],
updatedAt: DateTime.now(),
);
}
/// 获取指定Act
Act? getAct(String actId) {
try {
return acts.firstWhere((act) => act.id == actId);
} catch (e) {
return null;
}
}
/// 获取指定Chapter
Chapter? getChapter(String actId, String chapterId) {
final act = getAct(actId);
if (act == null) return null;
try {
return act.chapters.firstWhere((chapter) => chapter.id == chapterId);
} catch (e) {
return null;
}
}
/// 根据章节ID直接获取章节不需要知道Act ID
Chapter? getChapterById(String chapterId) {
for (final act in acts) {
try {
final chapter =
act.chapters.firstWhere((chapter) => chapter.id == chapterId);
return chapter;
} catch (e) {
// 继续查找下一个act
}
}
return null;
}
/// 获取指定Scene
Scene? getScene(String actId, String chapterId, {String? sceneId}) {
final chapter = getChapter(actId, chapterId);
if (chapter == null) return null;
if (sceneId != null) {
// 如果提供了sceneId则获取特定Scene
return chapter.getScene(sceneId);
} else if (chapter.scenes.isNotEmpty) {
// 否则返回第一个Scene
return chapter.scenes.first;
}
return null;
}
/// 获取上下文章节前后n章
List<Chapter> getContextChapters(String chapterId, int n) {
// 提取所有章节
List<Chapter> allChapters = [];
for (final act in acts) {
allChapters.addAll(act.chapters);
}
// 按order排序
allChapters.sort((a, b) => a.order.compareTo(b.order));
// 找到当前章节的索引
int currentIndex =
allChapters.indexWhere((chapter) => chapter.id == chapterId);
if (currentIndex == -1) {
// 如果找不到当前章节返回前n章
return allChapters.take(n).toList();
}
// 计算前后n章的范围
int startIndex = (currentIndex - n) < 0 ? 0 : (currentIndex - n);
int endIndex = (currentIndex + n) >= allChapters.length
? allChapters.length - 1
: (currentIndex + n);
// 提取前后n章
return allChapters.sublist(startIndex, endIndex + 1);
}
/// 更新最后编辑的章节ID
Novel updateLastEditedChapter(String chapterId) {
return copyWith(
lastEditedChapterId: chapterId,
updatedAt: DateTime.now(),
);
}
}
/// 幕模型如Act 1, Act 2等
class Act {
Act({
required this.id,
required this.title,
required this.order,
this.chapters = const [],
});
/// 从JSON创建Act实例
factory Act.fromJson(Map<String, dynamic> json) {
List<Chapter> parsedChapters = [];
if (json['chapters'] != null && json['chapters'] is List) {
parsedChapters = (json['chapters'] as List<dynamic>)
.map((chapterJson) =>
Chapter.fromJson(chapterJson as Map<String, dynamic>))
.toList();
}
return Act(
id: json['id'] as String,
title: json['title'] as String,
order: json['order'] as int,
chapters: parsedChapters, // 使用解析后的列表
);
}
final String id;
final String title;
final int order;
final List<Chapter> chapters;
/// 计算Act的总字数
int get wordCount {
return chapters.fold(0, (sum, chapter) => sum + chapter.wordCount);
}
/// 转换为JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'order': order,
'chapters': chapters.map((chapter) => chapter.toJson()).toList(),
};
}
/// 创建Act的副本
Act copyWith({
String? id,
String? title,
int? order,
List<Chapter>? chapters,
}) {
return Act(
id: id ?? this.id,
title: title ?? this.title,
order: order ?? this.order,
chapters: chapters ?? this.chapters,
);
}
/// 添加一个新的Chapter
Act addChapter(String title) {
// 创建一个默认的Scene
final defaultScene = Scene.createEmpty();
final newChapter = Chapter(
id: 'chapter_${DateTime.now().millisecondsSinceEpoch}',
title: title,
order: chapters.length + 1,
scenes: [defaultScene], // 包含一个默认的Scene
);
return copyWith(
chapters: [...chapters, newChapter],
);
}
/// 获取指定Chapter
Chapter? getChapter(String chapterId) {
try {
return chapters.firstWhere((chapter) => chapter.id == chapterId);
} catch (e) {
return null;
}
}
}
/// 章节模型
class Chapter {
Chapter({
required this.id,
required this.title,
required this.order,
this.scenes = const [],
this.sceneIds = const [], // 添加 sceneIds 字段
});
/// 从JSON创建Chapter实例
factory Chapter.fromJson(Map<String, dynamic> json) {
List<Scene> parsedScenes = [];
List<String> parsedSceneIds = [];
// 解析场景列表
if (json['scenes'] != null && json['scenes'] is List) {
parsedScenes = (json['scenes'] as List<dynamic>)
.map((sceneJson) => Scene.fromJson(sceneJson as Map<String, dynamic>))
.toList();
}
// 解析场景ID列表
if (json['sceneIds'] != null && json['sceneIds'] is List) {
parsedSceneIds = (json['sceneIds'] as List<dynamic>)
.map((id) => id.toString())
.toList();
}
return Chapter(
id: json['id'] as String,
title: json['title'] as String,
order: json['order'] as int,
scenes: parsedScenes,
sceneIds: parsedSceneIds, // 保存场景ID列表
);
}
final String id;
final String title;
final int order;
final List<Scene> scenes;
final List<String> sceneIds; // 保存从后端返回的场景ID列表
/// 计算章节的总字数
int get wordCount {
return scenes.fold(0, (sum, scene) => sum + scene.wordCount);
}
/// 获取场景总数scenes列表或sceneIds列表中的较大值
int get sceneCount {
int scenesLength = scenes.length;
int sceneIdsLength = sceneIds.length;
int result = scenesLength > sceneIdsLength ? scenesLength : sceneIdsLength;
//AppLogger.d('Chapter', '章节 $id 场景计数: scenes=$scenesLength, sceneIds=$sceneIdsLength, 取较大值=$result');
return result;
}
/// 转换为JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'order': order,
'scenes': scenes.map((scene) => scene.toJson()).toList(),
'sceneIds': sceneIds, // 添加场景ID列表
};
}
/// 创建Chapter的副本
Chapter copyWith({
String? id,
String? title,
int? order,
List<Scene>? scenes,
List<String>? sceneIds, // 添加sceneIds参数
}) {
return Chapter(
id: id ?? this.id,
title: title ?? this.title,
order: order ?? this.order,
scenes: scenes ?? this.scenes,
sceneIds: sceneIds ?? this.sceneIds, // 设置sceneIds
);
}
/// 添加一个新的Scene
void addScene(Scene newScene) {
scenes.add(newScene);
}
/// 获取指定Scene
Scene? getScene(String sceneId) {
try {
return scenes.firstWhere((scene) => scene.id == sceneId);
} catch (e) {
return null;
}
}
/// 更新指定Scene
Chapter updateScene(String sceneId, Scene updatedScene) {
final updatedScenes = scenes.map((scene) {
if (scene.id == sceneId) {
return updatedScene;
}
return scene;
}).toList();
return copyWith(scenes: updatedScenes);
}
}
/// 场景模型
class Scene {
Scene({
required this.id,
required this.content,
required this.wordCount,
required this.summary,
required this.lastEdited,
this.title = '',
this.actId = '',
this.chapterId = '',
this.version = 1,
this.history = const [],
});
/// 从JSON创建Scene实例
factory Scene.fromJson(Map<String, dynamic> json) {
// 创建安全的Summary对象
Summary summaryObj;
try {
// 处理summary字段 - 可能是字符串(后端)或对象(前端)
if (json.containsKey('summary')) {
final summaryData = json['summary'];
if (summaryData is Map<String, dynamic>) {
// 如果是对象格式,直接解析
summaryObj = Summary.fromJson(summaryData);
} else if (summaryData is String) {
// 如果是字符串格式后端发送的创建Summary对象
final sceneId = json['id'] ?? DateTime.now().millisecondsSinceEpoch.toString();
summaryObj = Summary(
id: '${sceneId}_summary',
content: summaryData,
);
} else {
// 其他格式创建默认Summary
final sceneId = json['id'] ?? DateTime.now().millisecondsSinceEpoch.toString();
summaryObj = Summary(
id: '${sceneId}_summary',
content: '',
);
AppLogger.w('Scene.fromJson', '场景 $sceneId 的摘要字段类型不支持: ${summaryData.runtimeType}');
}
} else {
// 创建默认Summary
final sceneId = json['id'] ?? DateTime.now().millisecondsSinceEpoch.toString();
summaryObj = Summary(
id: '${sceneId}_summary',
content: '',
);
AppLogger.w('Scene.fromJson', '场景 $sceneId 缺少摘要字段,已创建默认摘要');
}
} catch (e) {
// 处理任何异常创建默认Summary
final sceneId = json['id'] ?? DateTime.now().millisecondsSinceEpoch.toString();
summaryObj = Summary(
id: '${sceneId}_summary',
content: '',
);
AppLogger.e('Scene.fromJson', '解析场景 $sceneId 的摘要时出错', e);
}
// 安全解析lastEdited字段支持多种日期格式
DateTime lastEditedDate;
try {
if (json.containsKey('lastEdited') && json['lastEdited'] != null) {
final lastEditedStr = json['lastEdited'].toString();
lastEditedDate = _parseDateTime(lastEditedStr);
} else if (json.containsKey('updatedAt') && json['updatedAt'] != null) {
// 兼容后端可能使用updatedAt字段
final updatedAtStr = json['updatedAt'].toString();
lastEditedDate = _parseDateTime(updatedAtStr);
} else {
lastEditedDate = DateTime.now();
AppLogger.w('Scene.fromJson', '场景 ${json['id']} 缺少时间字段,使用当前时间');
}
} catch (e) {
lastEditedDate = DateTime.now();
AppLogger.w('Scene.fromJson', '解析场景 ${json['id']} 的时间字段失败,使用当前时间', e);
}
return Scene(
id: json['id'] ?? DateTime.now().millisecondsSinceEpoch.toString(),
content: json['content'] ?? '',
wordCount: json['wordCount'] ?? 0,
summary: summaryObj,
lastEdited: lastEditedDate,
title: json['title'] ?? '',
actId: json['actId'] ?? '',
chapterId: json['chapterId'] ?? '',
version: json['version'] ?? 1,
history: [],
);
}
final String id;
final String content;
final int wordCount;
final Summary summary;
final DateTime lastEdited;
final String title;
final String actId;
final String chapterId;
final int version;
final List<HistoryEntry> history;
/// 解析多种日期格式的工具方法
static DateTime _parseDateTime(String dateTimeStr) {
if (dateTimeStr.isEmpty) {
return DateTime.now();
}
try {
// 尝试标准ISO格式
return DateTime.parse(dateTimeStr);
} catch (e1) {
try {
// 尝试处理带毫秒的格式 "yyyy-MM-dd'T'HH:mm:ss.SSS"
if (dateTimeStr.contains('T') && dateTimeStr.contains('.')) {
// 如果包含时区信息,先移除
String cleanStr = dateTimeStr;
if (cleanStr.endsWith('Z')) {
cleanStr = cleanStr.substring(0, cleanStr.length - 1);
}
if (cleanStr.contains('+') || cleanStr.lastIndexOf('-') > 10) {
// 移除时区偏移
final timeZoneIndex = cleanStr.lastIndexOf('+') > cleanStr.lastIndexOf('-')
? cleanStr.lastIndexOf('+')
: cleanStr.lastIndexOf('-');
if (timeZoneIndex > 10) {
cleanStr = cleanStr.substring(0, timeZoneIndex);
}
}
return DateTime.parse(cleanStr);
}
// 尝试其他常见格式
// 格式yyyy-MM-dd HH:mm:ss
if (dateTimeStr.contains(' ') && !dateTimeStr.contains('T')) {
final parts = dateTimeStr.split(' ');
if (parts.length == 2) {
final datePart = parts[0];
final timePart = parts[1];
final isoStr = '${datePart}T$timePart';
return DateTime.parse(isoStr);
}
}
throw e1; // 如果都失败了,抛出原始异常
} catch (e2) {
AppLogger.w('Scene._parseDateTime', '无法解析日期格式: $dateTimeStr,使用当前时间');
return DateTime.now();
}
}
}
/// 转换为JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'content': content,
'wordCount': wordCount,
'summary': summary.toJson(),
'lastEdited': lastEdited.toIso8601String(),
'title': title,
'actId': actId,
'chapterId': chapterId,
'version': version,
'history': history.map((entry) => entry.toJson()).toList(),
};
}
/// 创建Scene的副本
Scene copyWith({
String? id,
String? content,
int? wordCount,
Summary? summary,
DateTime? lastEdited,
String? title,
String? actId,
String? chapterId,
int? version,
List<HistoryEntry>? history,
}) {
return Scene(
id: id ?? this.id,
content: content ?? this.content,
wordCount: wordCount ?? this.wordCount,
summary: summary ?? this.summary,
lastEdited: lastEdited ?? this.lastEdited,
title: title ?? this.title,
actId: actId ?? this.actId,
chapterId: chapterId ?? this.chapterId,
version: version ?? this.version,
history: history ?? this.history,
);
}
/// 创建一个空的场景
static Scene createEmpty() {
const defaultContent = '{"ops":[{"insert":"\\n"}]}'; // <-- 确保是这个值
final now = DateTime.now();
return Scene(
id: DateTime.now().millisecondsSinceEpoch.toString(),
content: defaultContent,
wordCount: 0,
summary: Summary(
id: '${DateTime.now().millisecondsSinceEpoch}_summary',
content: '',
),
lastEdited: now,
title: '',
actId: '',
chapterId: '',
version: 1,
history: [],
);
}
/// 创建一个默认的场景
static Scene createDefault(String sceneIdBase) {
// 使用正确Quill Delta格式包含ops对象的内容
const defaultContent = '{"ops":[{"insert":"\\n"}]}';
final now = DateTime.now();
return Scene(
id: sceneIdBase,
content: defaultContent,
wordCount: 0,
summary: Summary(
id: '${sceneIdBase}_summary',
content: '',
),
lastEdited: now,
title: '新场景',
actId: '',
chapterId: '',
version: 1,
history: [],
);
}
}
/// 摘要模型
class Summary {
Summary({
required this.id,
required this.content,
});
/// 从JSON创建Summary实例
factory Summary.fromJson(Map<String, dynamic> json) {
return Summary(
id: json['id'] as String,
content: json['content'] as String,
);
}
final String id;
final String content;
/// 转换为JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'content': content,
};
}
/// 创建Summary的副本
Summary copyWith({
String? id,
String? content,
}) {
return Summary(
id: id ?? this.id,
content: content ?? this.content,
);
}
/// 创建一个空的摘要
static Summary createEmpty() {
return Summary(
id: 'summary_${DateTime.now().millisecondsSinceEpoch}',
content: '',
);
}
}
class HistoryEntry {
HistoryEntry({
this.content,
required this.updatedAt,
required this.updatedBy,
required this.reason,
});
factory HistoryEntry.fromJson(Map<String, dynamic> json) {
DateTime updatedAt;
try {
updatedAt = DateTime.parse(json['updatedAt']);
} catch (e) {
updatedAt = DateTime.now();
}
return HistoryEntry(
content: json['content'],
updatedAt: updatedAt,
updatedBy: json['updatedBy'] ?? 'unknown',
reason: json['reason'] ?? '',
);
}
final String? content;
final DateTime updatedAt;
final String updatedBy;
final String reason;
Map<String, dynamic> toJson() => {
'content': content,
'updatedAt': updatedAt.toIso8601String(),
'updatedBy': updatedBy,
'reason': reason,
};
}
/// 作者信息模型
class Author {
Author({
required this.id,
required this.username,
});
/// 从JSON创建Author实例
factory Author.fromJson(Map<String, dynamic> json) {
return Author(
id: json['id'] ?? '',
username: json['username'] ?? '未知作者',
);
}
final String id;
final String username;
/// 转换为JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'username': username,
};
}
/// 创建Author的副本
Author copyWith({
String? id,
String? username,
}) {
return Author(
id: id ?? this.id,
username: username ?? this.username,
);
}
}

View File

@@ -0,0 +1,212 @@
import 'package:equatable/equatable.dart';
import 'novel_structure.dart';
class NovelSummary extends Equatable {
const NovelSummary({
required this.id,
required this.title,
this.coverUrl = '',
required this.lastEditTime,
this.wordCount = 0,
this.readTime = 0,
this.version = 1,
this.seriesName = '',
this.completionPercentage = 0.0,
this.lastEditedChapterId,
this.author,
this.contributors = const [],
this.actCount = 0,
this.chapterCount = 0,
this.sceneCount = 0,
this.description = '',
required this.serverUpdatedAt,
this.localUpdatedAt,
this.isCached = false,
this.needsSync = false,
this.lastReadTime,
});
// 从JSON转换方法
factory NovelSummary.fromJson(Map<String, dynamic> json) {
return NovelSummary(
id: json['id'],
title: json['title'],
coverUrl: json['coverUrl'] ?? '',
lastEditTime: DateTime.parse(json['lastEditTime']),
wordCount: json['wordCount'] ?? 0,
readTime: json['readTime'] ?? 0,
version: json['version'] ?? 1,
seriesName: json['seriesName'] ?? '',
completionPercentage: json['completionPercentage']?.toDouble() ?? 0.0,
lastEditedChapterId: json['lastEditedChapterId'],
author: json['author'],
contributors: (json['contributors'] as List?)?.cast<String>() ?? const [],
actCount: json['actCount'] ?? 0,
chapterCount: json['chapterCount'] ?? 0,
sceneCount: json['sceneCount'] ?? 0,
description: json['description'] ?? '',
serverUpdatedAt: json['serverUpdatedAt'] != null
? DateTime.parse(json['serverUpdatedAt'])
: DateTime.parse(json['lastEditTime']),
localUpdatedAt: json['localUpdatedAt'] != null
? DateTime.parse(json['localUpdatedAt'])
: null,
isCached: json['isCached'] ?? false,
needsSync: json['needsSync'] ?? false,
lastReadTime: json['lastReadTime'] != null
? DateTime.parse(json['lastReadTime'])
: null,
);
}
// 从Novel对象转换方法
factory NovelSummary.fromNovel(Novel novel) {
return NovelSummary(
id: novel.id,
title: novel.title,
coverUrl: novel.coverUrl,
lastEditTime: novel.updatedAt,
wordCount: novel.wordCount,
readTime: novel.readTime,
version: novel.version,
seriesName: '', // Novel中没有seriesName字段使用空字符串
completionPercentage: 0.0, // 需要计算的字段暂时设为0
lastEditedChapterId: novel.lastEditedChapterId,
author: novel.author?.username,
contributors: novel.contributors,
actCount: novel.getActCount(),
chapterCount: novel.getChapterCount(),
sceneCount: novel.getSceneCount(),
description: '', // Novel中没有description字段使用空字符串
serverUpdatedAt: novel.updatedAt,
localUpdatedAt: null, // 初始时本地缓存时间为空
isCached: false, // 初始时未缓存
needsSync: false, // 初始时不需要同步
lastReadTime: null, // 初始时没有阅读时间
);
}
final String id;
final String title;
final String coverUrl;
final DateTime lastEditTime;
final int wordCount;
final int readTime; // 估计阅读时间(分钟)
final int version; // 文档版本号
final String seriesName;
final double completionPercentage;
final String? lastEditedChapterId;
final String? author;
final List<String> contributors; // 贡献者列表
final int actCount;
final int chapterCount;
final int sceneCount;
final String description; // 小说描述
final DateTime serverUpdatedAt; // 服务器端最新更新时间
final DateTime? localUpdatedAt; // 本地缓存的更新时间
final bool isCached; // 是否已在本地完整缓存
final bool needsSync; // 是否需要同步
final DateTime? lastReadTime; // 上次阅读时间
@override
List<Object?> get props => [
id,
title,
coverUrl,
lastEditTime,
wordCount,
readTime,
version,
seriesName,
completionPercentage,
lastEditedChapterId,
author,
contributors,
actCount,
chapterCount,
sceneCount,
description,
serverUpdatedAt,
localUpdatedAt,
isCached,
needsSync,
lastReadTime,
];
// 转换为JSON方法
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'coverUrl': coverUrl,
'lastEditTime': lastEditTime.toIso8601String(),
'wordCount': wordCount,
'readTime': readTime,
'version': version,
'seriesName': seriesName,
'completionPercentage': completionPercentage,
'lastEditedChapterId': lastEditedChapterId,
'author': author,
'contributors': contributors,
'actCount': actCount,
'chapterCount': chapterCount,
'sceneCount': sceneCount,
'description': description,
'serverUpdatedAt': serverUpdatedAt.toIso8601String(),
'localUpdatedAt': localUpdatedAt?.toIso8601String(),
'isCached': isCached,
'needsSync': needsSync,
'lastReadTime': lastReadTime?.toIso8601String(),
};
}
// 新增 copyWith 方法,方便状态更新
NovelSummary copyWith({
String? id,
String? title,
String? coverUrl,
DateTime? lastEditTime,
int? wordCount,
int? readTime,
int? version,
String? seriesName,
double? completionPercentage,
String? lastEditedChapterId,
String? author,
List<String>? contributors,
int? actCount,
int? chapterCount,
int? sceneCount,
String? description,
DateTime? serverUpdatedAt,
DateTime? localUpdatedAt,
bool? isCached,
bool? needsSync,
DateTime? lastReadTime,
}) {
return NovelSummary(
id: id ?? this.id,
title: title ?? this.title,
coverUrl: coverUrl ?? this.coverUrl,
lastEditTime: lastEditTime ?? this.lastEditTime,
wordCount: wordCount ?? this.wordCount,
readTime: readTime ?? this.readTime,
version: version ?? this.version,
seriesName: seriesName ?? this.seriesName,
completionPercentage: completionPercentage ?? this.completionPercentage,
lastEditedChapterId: lastEditedChapterId ?? this.lastEditedChapterId,
author: author ?? this.author,
contributors: contributors ?? this.contributors,
actCount: actCount ?? this.actCount,
chapterCount: chapterCount ?? this.chapterCount,
sceneCount: sceneCount ?? this.sceneCount,
description: description ?? this.description,
serverUpdatedAt: serverUpdatedAt ?? this.serverUpdatedAt,
localUpdatedAt: localUpdatedAt ?? this.localUpdatedAt,
isCached: isCached ?? this.isCached,
needsSync: needsSync ?? this.needsSync,
lastReadTime: lastReadTime ?? this.lastReadTime,
);
}
}

View File

@@ -0,0 +1,160 @@
import 'package:ainoval/models/novel_structure.dart';
import 'package:ainoval/models/scene_summary_dto.dart';
import 'package:ainoval/utils/logger.dart';
/// 包含场景摘要的小说DTO
/// 用于映射服务器返回的包含场景摘要的小说结构
class NovelWithSummariesDto {
final Novel novel;
final Map<String, List<SceneSummaryDto>> sceneSummariesByChapter;
NovelWithSummariesDto({
required this.novel,
required this.sceneSummariesByChapter,
});
/// 从JSON创建NovelWithSummariesDto实例
factory NovelWithSummariesDto.fromJson(Map<String, dynamic> json) {
try {
AppLogger.i('NovelWithSummariesDto', '开始解析小说和场景摘要数据');
// 确保novel字段存在且是Map类型
if (!json.containsKey('novel') || !(json['novel'] is Map<String, dynamic>)) {
AppLogger.w('NovelWithSummariesDto', '返回数据中缺少novel字段或格式不正确');
throw FormatException('返回数据缺少novel字段或格式不正确');
}
// 解析小说基本信息
final novelJson = json['novel'] as Map<String, dynamic>;
// 确保结构字段正确特别是acts字段
if (novelJson.containsKey('structure') && novelJson['structure'] is Map) {
final structureMap = novelJson['structure'] as Map<String, dynamic>;
// 检查并确保acts字段是List类型
if (structureMap.containsKey('acts') && !(structureMap['acts'] is List)) {
AppLogger.w('NovelWithSummariesDto', 'novel.structure.acts不是列表类型正在修正');
structureMap['acts'] = <Map<String, dynamic>>[];
}
} else {
// 如果没有structure字段或不是Map类型添加一个空的structure
novelJson['structure'] = {'acts': <Map<String, dynamic>>[]};
AppLogger.w('NovelWithSummariesDto', '返回数据中缺少novel.structure字段已添加空结构');
}
// 解析Novel
final novel = Novel.fromJson(novelJson);
AppLogger.i('NovelWithSummariesDto', '小说基本信息解析成功: ${novel.title}');
// 解析场景摘要
final sceneSummariesMap = <String, List<SceneSummaryDto>>{};
// 检查sceneSummariesByChapter字段是否存在且是Map类型
if (json.containsKey('sceneSummariesByChapter') && json['sceneSummariesByChapter'] is Map) {
final summariesData = json['sceneSummariesByChapter'] as Map<String, dynamic>;
summariesData.forEach((chapterId, summariesList) {
if (summariesList is List) {
try {
final sceneList = <SceneSummaryDto>[];
for (var summaryItem in summariesList) {
if (summaryItem is Map<String, dynamic>) {
sceneList.add(SceneSummaryDto.fromJson(summaryItem));
} else {
AppLogger.w('NovelWithSummariesDto', '场景摘要数据格式错误: $summaryItem');
}
}
if (sceneList.isNotEmpty) {
sceneSummariesMap[chapterId] = sceneList;
}
} catch (e) {
AppLogger.e('NovelWithSummariesDto', '解析章节 $chapterId 的场景摘要失败', e);
}
} else {
AppLogger.w('NovelWithSummariesDto', '章节 $chapterId 的场景摘要不是列表格式');
}
});
} else {
AppLogger.w('NovelWithSummariesDto', '返回数据中缺少sceneSummariesByChapter字段或格式不正确');
}
AppLogger.i('NovelWithSummariesDto', '解析完成,共有 ${sceneSummariesMap.length} 个章节包含场景摘要');
return NovelWithSummariesDto(
novel: novel,
sceneSummariesByChapter: sceneSummariesMap,
);
} catch (e) {
AppLogger.e('NovelWithSummariesDto', '从JSON创建NovelWithSummariesDto实例失败', e);
// 尝试创建一个空的对象,确保不会完全失败
try {
if (json.containsKey('novel') && json['novel'] is Map<String, dynamic>) {
// 尝试只解析小说部分
final novel = Novel.fromJson(json['novel'] as Map<String, dynamic>);
return NovelWithSummariesDto(
novel: novel,
sceneSummariesByChapter: {},
);
}
} catch (_) {
// 如果还是失败,创建一个完全空的对象
AppLogger.e('NovelWithSummariesDto', '尝试创建备用对象也失败');
}
rethrow;
}
}
/// 将DTO中的场景摘要信息合并到Novel模型中
Novel mergeSceneSummariesToNovel() {
try {
// 创建小说的副本,避免修改原始模型
Novel updatedNovel = novel;
// 遍历小说中的卷和章节
final List<Act> updatedActs = novel.acts.map((act) {
final List<Chapter> updatedChapters = act.chapters.map((chapter) {
// 检查这个章节是否有场景摘要
if (sceneSummariesByChapter.containsKey(chapter.id)) {
final summaries = sceneSummariesByChapter[chapter.id]!;
// 根据场景摘要创建场景对象
final List<Scene> scenes = summaries.map((summaryDto) {
return Scene(
id: summaryDto.id,
content: '', // 摘要模式下不需要完整内容
wordCount: summaryDto.wordCount,
summary: Summary(
id: '${summaryDto.id}_summary',
content: summaryDto.summary,
),
lastEdited: summaryDto.updatedAt,
title: summaryDto.title,
chapterId: summaryDto.chapterId,
);
}).toList();
// 创建更新后的章节
return chapter.copyWith(scenes: scenes);
}
// 如果没有摘要信息,保持原样
return chapter;
}).toList();
// 创建更新后的卷
return act.copyWith(chapters: updatedChapters);
}).toList();
// 创建更新后的小说
updatedNovel = updatedNovel.copyWith(acts: updatedActs);
return updatedNovel;
} catch (e) {
AppLogger.e('NovelWithSummariesDto', '合并场景摘要到Novel模型失败', e);
return novel; // 出错时返回原始小说模型
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,719 @@
import 'package:json_annotation/json_annotation.dart';
import '../utils/date_time_parser.dart';
part 'public_model_config.g.dart';
/// 公共模型配置详细信息模型
@JsonSerializable()
class PublicModelConfigDetails {
/// 配置ID
final String? id;
/// 提供商名称
final String provider;
/// 模型ID
final String modelId;
/// 模型显示名称
final String? displayName;
/// 是否启用
final bool? enabled;
/// API Endpoint
final String? apiEndpoint;
/// 整体验证状态
final bool? isValidated;
/// API Key池状态摘要 (格式: "有效数量/总数量")
final String? apiKeyPoolStatus;
/// API Key池详情
final List<ApiKeyStatus>? apiKeyStatuses;
/// 授权功能列表 - 使用自定义转换
@JsonKey(fromJson: _enabledFeaturesFromJson, toJson: _enabledFeaturesToJson)
final List<String>? enabledForFeatures;
/// 积分汇率乘数
final double? creditRateMultiplier;
/// 最大并发请求数
final int? maxConcurrentRequests;
/// 每日请求限制
final int? dailyRequestLimit;
/// 每小时请求限制
final int? hourlyRequestLimit;
/// 优先级
final int? priority;
/// 描述
final String? description;
/// 标签
final List<String>? tags;
/// 创建时间 - 使用自定义转换
@JsonKey(fromJson: _parseDateTime, toJson: _dateTimeToJson)
final DateTime? createdAt;
/// 更新时间 - 使用自定义转换
@JsonKey(fromJson: _parseDateTime, toJson: _dateTimeToJson)
final DateTime? updatedAt;
/// 创建者用户ID
final String? createdBy;
/// 最后修改者用户ID
final String? updatedBy;
/// 定价信息
final PricingInfo? pricingInfo;
/// 使用统计信息
final UsageStatistics? usageStatistics;
PublicModelConfigDetails({
this.id,
required this.provider,
required this.modelId,
this.displayName,
this.enabled,
this.apiEndpoint,
this.isValidated,
this.apiKeyPoolStatus,
this.apiKeyStatuses,
this.enabledForFeatures,
this.creditRateMultiplier,
this.maxConcurrentRequests,
this.dailyRequestLimit,
this.hourlyRequestLimit,
this.priority,
this.description,
this.tags,
this.createdAt,
this.updatedAt,
this.createdBy,
this.updatedBy,
this.pricingInfo,
this.usageStatistics,
});
factory PublicModelConfigDetails.fromJson(Map<String, dynamic> json) =>
_$PublicModelConfigDetailsFromJson(json);
Map<String, dynamic> toJson() => _$PublicModelConfigDetailsToJson(this);
/// 自定义转换函数:从后端枚举转换为字符串列表
static List<String>? _enabledFeaturesFromJson(dynamic json) {
if (json == null) return null;
if (json is List) {
return json.map((item) {
if (item is String) {
return item;
} else if (item is Map && item.containsKey('name')) {
// 处理枚举对象 {name: "AI_CHAT", ordinal: 0}
return item['name'] as String;
} else {
// 直接转换为字符串
return item.toString();
}
}).toList();
}
return null;
}
/// 自定义转换函数从字符串列表转换为JSON
static List<String>? _enabledFeaturesToJson(List<String>? features) {
return features;
}
/// 自定义时间解析函数使用date_time_parser.dart
static DateTime? _parseDateTime(dynamic json) {
if (json == null) return null;
try {
return parseBackendDateTime(json);
} catch (e) {
return null;
}
}
/// 自定义时间序列化函数
static String? _dateTimeToJson(DateTime? dateTime) {
return dateTime?.toIso8601String();
}
}
/// API Key状态不包含API Key值
@JsonSerializable()
class ApiKeyStatus {
/// 是否验证通过
final bool? isValid;
/// 验证错误信息
final String? validationError;
/// 最近验证时间 - 使用自定义转换
@JsonKey(fromJson: _parseDateTime, toJson: _dateTimeToJson)
final DateTime? lastValidatedAt;
/// 备注
final String? note;
ApiKeyStatus({
this.isValid,
this.validationError,
this.lastValidatedAt,
this.note,
});
factory ApiKeyStatus.fromJson(Map<String, dynamic> json) =>
_$ApiKeyStatusFromJson(json);
Map<String, dynamic> toJson() => _$ApiKeyStatusToJson(this);
/// 自定义时间解析函数使用date_time_parser.dart
static DateTime? _parseDateTime(dynamic json) {
if (json == null) return null;
try {
return parseBackendDateTime(json);
} catch (e) {
return null;
}
}
/// 自定义时间序列化函数
static String? _dateTimeToJson(DateTime? dateTime) {
return dateTime?.toIso8601String();
}
}
/// API Key状态包含API Key值- 仅供管理员使用
@JsonSerializable()
class ApiKeyWithStatus {
/// API Key值
final String? apiKey;
/// 是否验证通过
final bool? isValid;
/// 验证错误信息
final String? validationError;
/// 最近验证时间 - 使用自定义转换
@JsonKey(fromJson: _parseDateTime, toJson: _dateTimeToJson)
final DateTime? lastValidatedAt;
/// 备注
final String? note;
ApiKeyWithStatus({
this.apiKey,
this.isValid,
this.validationError,
this.lastValidatedAt,
this.note,
});
factory ApiKeyWithStatus.fromJson(Map<String, dynamic> json) =>
_$ApiKeyWithStatusFromJson(json);
Map<String, dynamic> toJson() => _$ApiKeyWithStatusToJson(this);
/// 自定义时间解析函数使用date_time_parser.dart
static DateTime? _parseDateTime(dynamic json) {
if (json == null) return null;
try {
return parseBackendDateTime(json);
} catch (e) {
return null;
}
}
/// 自定义时间序列化函数
static String? _dateTimeToJson(DateTime? dateTime) {
return dateTime?.toIso8601String();
}
}
/// 定价信息
@JsonSerializable()
class PricingInfo {
/// 模型名称
final String? modelName;
/// 输入token价格每1000个token的美元价格
final double? inputPricePerThousandTokens;
/// 输出token价格每1000个token的美元价格
final double? outputPricePerThousandTokens;
/// 统一价格(如果输入输出使用相同价格)
final double? unifiedPricePerThousandTokens;
/// 最大上下文token数
final int? maxContextTokens;
/// 是否支持流式输出
final bool? supportsStreaming;
/// 定价数据更新时间 - 使用自定义转换
@JsonKey(fromJson: _parseDateTime, toJson: _dateTimeToJson)
final DateTime? pricingUpdatedAt;
/// 是否有定价数据
final bool? hasPricingData;
PricingInfo({
this.modelName,
this.inputPricePerThousandTokens,
this.outputPricePerThousandTokens,
this.unifiedPricePerThousandTokens,
this.maxContextTokens,
this.supportsStreaming,
this.pricingUpdatedAt,
this.hasPricingData,
});
factory PricingInfo.fromJson(Map<String, dynamic> json) =>
_$PricingInfoFromJson(json);
Map<String, dynamic> toJson() => _$PricingInfoToJson(this);
/// 自定义时间解析函数使用date_time_parser.dart
static DateTime? _parseDateTime(dynamic json) {
if (json == null) return null;
try {
return parseBackendDateTime(json);
} catch (e) {
return null;
}
}
/// 自定义时间序列化函数
static String? _dateTimeToJson(DateTime? dateTime) {
return dateTime?.toIso8601String();
}
}
/// 使用统计信息
@JsonSerializable()
class UsageStatistics {
/// 总请求数
final int? totalRequests;
/// 总输入token数
final int? totalInputTokens;
/// 总输出token数
final int? totalOutputTokens;
/// 总token数
final int? totalTokens;
/// 总成本
final double? totalCost;
/// 平均每请求成本
final double? averageCostPerRequest;
/// 平均每token成本
final double? averageCostPerToken;
/// 最近30天请求数
final int? last30DaysRequests;
/// 最近30天成本
final double? last30DaysCost;
/// 是否有使用数据
final bool? hasUsageData;
UsageStatistics({
this.totalRequests,
this.totalInputTokens,
this.totalOutputTokens,
this.totalTokens,
this.totalCost,
this.averageCostPerRequest,
this.averageCostPerToken,
this.last30DaysRequests,
this.last30DaysCost,
this.hasUsageData,
});
factory UsageStatistics.fromJson(Map<String, dynamic> json) =>
_$UsageStatisticsFromJson(json);
Map<String, dynamic> toJson() => _$UsageStatisticsToJson(this);
}
/// 公共模型配置请求模型
@JsonSerializable()
class PublicModelConfigRequest {
/// 提供商名称
final String provider;
/// 模型ID
final String modelId;
/// 模型显示名称
final String? displayName;
/// 是否启用
final bool? enabled;
/// API Key列表
final List<ApiKeyRequest>? apiKeys;
/// API Endpoint
final String? apiEndpoint;
/// 授权功能列表
final List<String>? enabledForFeatures;
/// 积分汇率乘数
final double? creditRateMultiplier;
/// 最大并发请求数
final int? maxConcurrentRequests;
/// 每日请求限制
final int? dailyRequestLimit;
/// 每小时请求限制
final int? hourlyRequestLimit;
/// 优先级
final int? priority;
/// 描述
final String? description;
/// 标签
final List<String>? tags;
PublicModelConfigRequest({
required this.provider,
required this.modelId,
this.displayName,
this.enabled,
this.apiKeys,
this.apiEndpoint,
this.enabledForFeatures,
this.creditRateMultiplier,
this.maxConcurrentRequests,
this.dailyRequestLimit,
this.hourlyRequestLimit,
this.priority,
this.description,
this.tags,
});
factory PublicModelConfigRequest.fromJson(Map<String, dynamic> json) =>
_$PublicModelConfigRequestFromJson(json);
Map<String, dynamic> toJson() => _$PublicModelConfigRequestToJson(this);
}
/// API Key请求
@JsonSerializable()
class ApiKeyRequest {
/// API Key
final String apiKey;
/// 备注
final String? note;
ApiKeyRequest({
required this.apiKey,
this.note,
});
factory ApiKeyRequest.fromJson(Map<String, dynamic> json) =>
_$ApiKeyRequestFromJson(json);
Map<String, dynamic> toJson() => _$ApiKeyRequestToJson(this);
}
/// 公共模型配置详细信息模型包含API Keys- 仅供管理员使用
@JsonSerializable()
class PublicModelConfigWithKeys {
/// 配置ID
final String? id;
/// 提供商名称
final String provider;
/// 模型ID
final String modelId;
/// 模型显示名称
final String? displayName;
/// 是否启用
final bool? enabled;
/// API Endpoint
final String? apiEndpoint;
/// 整体验证状态
final bool? isValidated;
/// API Key池状态摘要 (格式: "有效数量/总数量")
final String? apiKeyPoolStatus;
/// API Key池详情包含实际的Key值
final List<ApiKeyWithStatus>? apiKeyStatuses;
/// 授权功能列表 - 使用自定义转换
@JsonKey(fromJson: _enabledFeaturesFromJson, toJson: _enabledFeaturesToJson)
final List<String>? enabledForFeatures;
/// 积分汇率乘数
final double? creditRateMultiplier;
/// 最大并发请求数
final int? maxConcurrentRequests;
/// 每日请求限制
final int? dailyRequestLimit;
/// 每小时请求限制
final int? hourlyRequestLimit;
/// 优先级
final int? priority;
/// 描述
final String? description;
/// 标签
final List<String>? tags;
/// 创建时间 - 使用自定义转换
@JsonKey(fromJson: _parseDateTime, toJson: _dateTimeToJson)
final DateTime? createdAt;
/// 更新时间 - 使用自定义转换
@JsonKey(fromJson: _parseDateTime, toJson: _dateTimeToJson)
final DateTime? updatedAt;
/// 创建者用户ID
final String? createdBy;
/// 最后修改者用户ID
final String? updatedBy;
/// 定价信息
final PricingInfo? pricingInfo;
/// 使用统计信息
final UsageStatistics? usageStatistics;
PublicModelConfigWithKeys({
this.id,
required this.provider,
required this.modelId,
this.displayName,
this.enabled,
this.apiEndpoint,
this.isValidated,
this.apiKeyPoolStatus,
this.apiKeyStatuses,
this.enabledForFeatures,
this.creditRateMultiplier,
this.maxConcurrentRequests,
this.dailyRequestLimit,
this.hourlyRequestLimit,
this.priority,
this.description,
this.tags,
this.createdAt,
this.updatedAt,
this.createdBy,
this.updatedBy,
this.pricingInfo,
this.usageStatistics,
});
factory PublicModelConfigWithKeys.fromJson(Map<String, dynamic> json) =>
_$PublicModelConfigWithKeysFromJson(json);
Map<String, dynamic> toJson() => _$PublicModelConfigWithKeysToJson(this);
/// 自定义转换函数:从后端枚举转换为字符串列表
static List<String>? _enabledFeaturesFromJson(dynamic json) {
if (json == null) return null;
if (json is List) {
return json.map((item) {
if (item is String) {
return item;
} else if (item is Map && item.containsKey('name')) {
// 处理枚举对象 {name: "AI_CHAT", ordinal: 0}
return item['name'] as String;
} else {
// 直接转换为字符串
return item.toString();
}
}).toList();
}
return null;
}
/// 自定义转换函数从字符串列表转换为JSON
static List<String>? _enabledFeaturesToJson(List<String>? features) {
return features;
}
/// 自定义时间解析函数使用date_time_parser.dart
static DateTime? _parseDateTime(dynamic json) {
if (json == null) return null;
try {
return parseBackendDateTime(json);
} catch (e) {
return null;
}
}
/// 自定义时间序列化函数
static String? _dateTimeToJson(DateTime? dateTime) {
return dateTime?.toIso8601String();
}
}
/// 公共模型响应DTO对应后端的PublicModelResponseDto
/// 只包含向前端暴露的安全信息不含API Keys等敏感数据
@JsonSerializable()
class PublicModel {
/// 模型ID
final String id;
/// 提供商 (如: openai, anthropic, google等)
final String provider;
/// 模型标识符 (如: gpt-4, claude-3-sonnet)
final String modelId;
/// 显示名称
final String displayName;
/// 模型描述
final String? description;
/// 积分倍率 (如: 1.0 表示标准倍率, 1.5 表示1.5倍积分)
final double? creditRateMultiplier;
/// 支持的AI功能列表
final List<String>? supportedFeatures;
/// 模型标签 (如: ["快速", "高质量", "多语言"])
final List<String>? tags;
/// 性能指标
final PerformanceMetrics? performanceMetrics;
/// 限制信息
final LimitationInfo? limitations;
/// 优先级 (用于前端排序)
final int? priority;
/// 是否推荐使用
final bool? recommended;
PublicModel({
required this.id,
required this.provider,
required this.modelId,
required this.displayName,
this.description,
this.creditRateMultiplier,
this.supportedFeatures,
this.tags,
this.performanceMetrics,
this.limitations,
this.priority,
this.recommended,
});
factory PublicModel.fromJson(Map<String, dynamic> json) =>
_$PublicModelFromJson(json);
Map<String, dynamic> toJson() => _$PublicModelToJson(this);
/// 获取格式化的积分倍率显示文本
String get creditMultiplierDisplay {
if (creditRateMultiplier == null) return '';
if (creditRateMultiplier! == 1.0) return '';
return '${creditRateMultiplier!.toStringAsFixed(1)}x积分';
}
/// 是否为公共模型总是返回true用于区分私有模型
bool get isPublic => true;
}
/// 性能指标
@JsonSerializable()
class PerformanceMetrics {
/// 平均响应时间(毫秒)
final int? averageResponseTimeMs;
/// 吞吐量(每分钟请求数)
final int? throughputPerMinute;
/// 可用性百分比
final double? availabilityPercentage;
/// 质量评分1-10
final double? qualityScore;
PerformanceMetrics({
this.averageResponseTimeMs,
this.throughputPerMinute,
this.availabilityPercentage,
this.qualityScore,
});
factory PerformanceMetrics.fromJson(Map<String, dynamic> json) =>
_$PerformanceMetricsFromJson(json);
Map<String, dynamic> toJson() => _$PerformanceMetricsToJson(this);
}
/// 限制信息
@JsonSerializable()
class LimitationInfo {
/// 最大上下文长度
final int? maxContextLength;
/// 每分钟请求限制
final int? requestsPerMinute;
/// 每小时请求限制
final int? requestsPerHour;
/// 每日请求限制
final int? requestsPerDay;
/// 是否支持流式输出
final bool? supportsStreaming;
LimitationInfo({
this.maxContextLength,
this.requestsPerMinute,
this.requestsPerHour,
this.requestsPerDay,
this.supportsStreaming,
});
factory LimitationInfo.fromJson(Map<String, dynamic> json) =>
_$LimitationInfoFromJson(json);
Map<String, dynamic> toJson() => _$LimitationInfoToJson(this);
}

View File

@@ -0,0 +1,598 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'public_model_config.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PublicModelConfigDetails _$PublicModelConfigDetailsFromJson(
Map<String, dynamic> json) =>
$checkedCreate(
'PublicModelConfigDetails',
json,
($checkedConvert) {
final val = PublicModelConfigDetails(
id: $checkedConvert('id', (v) => v as String?),
provider: $checkedConvert('provider', (v) => v as String),
modelId: $checkedConvert('modelId', (v) => v as String),
displayName: $checkedConvert('displayName', (v) => v as String?),
enabled: $checkedConvert('enabled', (v) => v as bool?),
apiEndpoint: $checkedConvert('apiEndpoint', (v) => v as String?),
isValidated: $checkedConvert('isValidated', (v) => v as bool?),
apiKeyPoolStatus:
$checkedConvert('apiKeyPoolStatus', (v) => v as String?),
apiKeyStatuses: $checkedConvert(
'apiKeyStatuses',
(v) => (v as List<dynamic>?)
?.map((e) => ApiKeyStatus.fromJson(e as Map<String, dynamic>))
.toList()),
enabledForFeatures: $checkedConvert('enabledForFeatures',
(v) => PublicModelConfigDetails._enabledFeaturesFromJson(v)),
creditRateMultiplier: $checkedConvert(
'creditRateMultiplier', (v) => (v as num?)?.toDouble()),
maxConcurrentRequests: $checkedConvert(
'maxConcurrentRequests', (v) => (v as num?)?.toInt()),
dailyRequestLimit:
$checkedConvert('dailyRequestLimit', (v) => (v as num?)?.toInt()),
hourlyRequestLimit: $checkedConvert(
'hourlyRequestLimit', (v) => (v as num?)?.toInt()),
priority: $checkedConvert('priority', (v) => (v as num?)?.toInt()),
description: $checkedConvert('description', (v) => v as String?),
tags: $checkedConvert('tags',
(v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
createdAt: $checkedConvert(
'createdAt', (v) => PublicModelConfigDetails._parseDateTime(v)),
updatedAt: $checkedConvert(
'updatedAt', (v) => PublicModelConfigDetails._parseDateTime(v)),
createdBy: $checkedConvert('createdBy', (v) => v as String?),
updatedBy: $checkedConvert('updatedBy', (v) => v as String?),
pricingInfo: $checkedConvert(
'pricingInfo',
(v) => v == null
? null
: PricingInfo.fromJson(v as Map<String, dynamic>)),
usageStatistics: $checkedConvert(
'usageStatistics',
(v) => v == null
? null
: UsageStatistics.fromJson(v as Map<String, dynamic>)),
);
return val;
},
);
Map<String, dynamic> _$PublicModelConfigDetailsToJson(
PublicModelConfigDetails instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('id', instance.id);
val['provider'] = instance.provider;
val['modelId'] = instance.modelId;
writeNotNull('displayName', instance.displayName);
writeNotNull('enabled', instance.enabled);
writeNotNull('apiEndpoint', instance.apiEndpoint);
writeNotNull('isValidated', instance.isValidated);
writeNotNull('apiKeyPoolStatus', instance.apiKeyPoolStatus);
writeNotNull('apiKeyStatuses',
instance.apiKeyStatuses?.map((e) => e.toJson()).toList());
writeNotNull(
'enabledForFeatures',
PublicModelConfigDetails._enabledFeaturesToJson(
instance.enabledForFeatures));
writeNotNull('creditRateMultiplier', instance.creditRateMultiplier);
writeNotNull('maxConcurrentRequests', instance.maxConcurrentRequests);
writeNotNull('dailyRequestLimit', instance.dailyRequestLimit);
writeNotNull('hourlyRequestLimit', instance.hourlyRequestLimit);
writeNotNull('priority', instance.priority);
writeNotNull('description', instance.description);
writeNotNull('tags', instance.tags);
writeNotNull('createdAt',
PublicModelConfigDetails._dateTimeToJson(instance.createdAt));
writeNotNull('updatedAt',
PublicModelConfigDetails._dateTimeToJson(instance.updatedAt));
writeNotNull('createdBy', instance.createdBy);
writeNotNull('updatedBy', instance.updatedBy);
writeNotNull('pricingInfo', instance.pricingInfo?.toJson());
writeNotNull('usageStatistics', instance.usageStatistics?.toJson());
return val;
}
ApiKeyStatus _$ApiKeyStatusFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'ApiKeyStatus',
json,
($checkedConvert) {
final val = ApiKeyStatus(
isValid: $checkedConvert('isValid', (v) => v as bool?),
validationError:
$checkedConvert('validationError', (v) => v as String?),
lastValidatedAt: $checkedConvert(
'lastValidatedAt', (v) => ApiKeyStatus._parseDateTime(v)),
note: $checkedConvert('note', (v) => v as String?),
);
return val;
},
);
Map<String, dynamic> _$ApiKeyStatusToJson(ApiKeyStatus instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('isValid', instance.isValid);
writeNotNull('validationError', instance.validationError);
writeNotNull('lastValidatedAt',
ApiKeyStatus._dateTimeToJson(instance.lastValidatedAt));
writeNotNull('note', instance.note);
return val;
}
ApiKeyWithStatus _$ApiKeyWithStatusFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'ApiKeyWithStatus',
json,
($checkedConvert) {
final val = ApiKeyWithStatus(
apiKey: $checkedConvert('apiKey', (v) => v as String?),
isValid: $checkedConvert('isValid', (v) => v as bool?),
validationError:
$checkedConvert('validationError', (v) => v as String?),
lastValidatedAt: $checkedConvert(
'lastValidatedAt', (v) => ApiKeyWithStatus._parseDateTime(v)),
note: $checkedConvert('note', (v) => v as String?),
);
return val;
},
);
Map<String, dynamic> _$ApiKeyWithStatusToJson(ApiKeyWithStatus instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('apiKey', instance.apiKey);
writeNotNull('isValid', instance.isValid);
writeNotNull('validationError', instance.validationError);
writeNotNull('lastValidatedAt',
ApiKeyWithStatus._dateTimeToJson(instance.lastValidatedAt));
writeNotNull('note', instance.note);
return val;
}
PricingInfo _$PricingInfoFromJson(Map<String, dynamic> json) => $checkedCreate(
'PricingInfo',
json,
($checkedConvert) {
final val = PricingInfo(
modelName: $checkedConvert('modelName', (v) => v as String?),
inputPricePerThousandTokens: $checkedConvert(
'inputPricePerThousandTokens', (v) => (v as num?)?.toDouble()),
outputPricePerThousandTokens: $checkedConvert(
'outputPricePerThousandTokens', (v) => (v as num?)?.toDouble()),
unifiedPricePerThousandTokens: $checkedConvert(
'unifiedPricePerThousandTokens', (v) => (v as num?)?.toDouble()),
maxContextTokens:
$checkedConvert('maxContextTokens', (v) => (v as num?)?.toInt()),
supportsStreaming:
$checkedConvert('supportsStreaming', (v) => v as bool?),
pricingUpdatedAt: $checkedConvert(
'pricingUpdatedAt', (v) => PricingInfo._parseDateTime(v)),
hasPricingData: $checkedConvert('hasPricingData', (v) => v as bool?),
);
return val;
},
);
Map<String, dynamic> _$PricingInfoToJson(PricingInfo instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('modelName', instance.modelName);
writeNotNull(
'inputPricePerThousandTokens', instance.inputPricePerThousandTokens);
writeNotNull(
'outputPricePerThousandTokens', instance.outputPricePerThousandTokens);
writeNotNull(
'unifiedPricePerThousandTokens', instance.unifiedPricePerThousandTokens);
writeNotNull('maxContextTokens', instance.maxContextTokens);
writeNotNull('supportsStreaming', instance.supportsStreaming);
writeNotNull('pricingUpdatedAt',
PricingInfo._dateTimeToJson(instance.pricingUpdatedAt));
writeNotNull('hasPricingData', instance.hasPricingData);
return val;
}
UsageStatistics _$UsageStatisticsFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'UsageStatistics',
json,
($checkedConvert) {
final val = UsageStatistics(
totalRequests:
$checkedConvert('totalRequests', (v) => (v as num?)?.toInt()),
totalInputTokens:
$checkedConvert('totalInputTokens', (v) => (v as num?)?.toInt()),
totalOutputTokens:
$checkedConvert('totalOutputTokens', (v) => (v as num?)?.toInt()),
totalTokens:
$checkedConvert('totalTokens', (v) => (v as num?)?.toInt()),
totalCost:
$checkedConvert('totalCost', (v) => (v as num?)?.toDouble()),
averageCostPerRequest: $checkedConvert(
'averageCostPerRequest', (v) => (v as num?)?.toDouble()),
averageCostPerToken: $checkedConvert(
'averageCostPerToken', (v) => (v as num?)?.toDouble()),
last30DaysRequests: $checkedConvert(
'last30DaysRequests', (v) => (v as num?)?.toInt()),
last30DaysCost:
$checkedConvert('last30DaysCost', (v) => (v as num?)?.toDouble()),
hasUsageData: $checkedConvert('hasUsageData', (v) => v as bool?),
);
return val;
},
);
Map<String, dynamic> _$UsageStatisticsToJson(UsageStatistics instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('totalRequests', instance.totalRequests);
writeNotNull('totalInputTokens', instance.totalInputTokens);
writeNotNull('totalOutputTokens', instance.totalOutputTokens);
writeNotNull('totalTokens', instance.totalTokens);
writeNotNull('totalCost', instance.totalCost);
writeNotNull('averageCostPerRequest', instance.averageCostPerRequest);
writeNotNull('averageCostPerToken', instance.averageCostPerToken);
writeNotNull('last30DaysRequests', instance.last30DaysRequests);
writeNotNull('last30DaysCost', instance.last30DaysCost);
writeNotNull('hasUsageData', instance.hasUsageData);
return val;
}
PublicModelConfigRequest _$PublicModelConfigRequestFromJson(
Map<String, dynamic> json) =>
$checkedCreate(
'PublicModelConfigRequest',
json,
($checkedConvert) {
final val = PublicModelConfigRequest(
provider: $checkedConvert('provider', (v) => v as String),
modelId: $checkedConvert('modelId', (v) => v as String),
displayName: $checkedConvert('displayName', (v) => v as String?),
enabled: $checkedConvert('enabled', (v) => v as bool?),
apiKeys: $checkedConvert(
'apiKeys',
(v) => (v as List<dynamic>?)
?.map(
(e) => ApiKeyRequest.fromJson(e as Map<String, dynamic>))
.toList()),
apiEndpoint: $checkedConvert('apiEndpoint', (v) => v as String?),
enabledForFeatures: $checkedConvert('enabledForFeatures',
(v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
creditRateMultiplier: $checkedConvert(
'creditRateMultiplier', (v) => (v as num?)?.toDouble()),
maxConcurrentRequests: $checkedConvert(
'maxConcurrentRequests', (v) => (v as num?)?.toInt()),
dailyRequestLimit:
$checkedConvert('dailyRequestLimit', (v) => (v as num?)?.toInt()),
hourlyRequestLimit: $checkedConvert(
'hourlyRequestLimit', (v) => (v as num?)?.toInt()),
priority: $checkedConvert('priority', (v) => (v as num?)?.toInt()),
description: $checkedConvert('description', (v) => v as String?),
tags: $checkedConvert('tags',
(v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
);
return val;
},
);
Map<String, dynamic> _$PublicModelConfigRequestToJson(
PublicModelConfigRequest instance) {
final val = <String, dynamic>{
'provider': instance.provider,
'modelId': instance.modelId,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('displayName', instance.displayName);
writeNotNull('enabled', instance.enabled);
writeNotNull('apiKeys', instance.apiKeys?.map((e) => e.toJson()).toList());
writeNotNull('apiEndpoint', instance.apiEndpoint);
writeNotNull('enabledForFeatures', instance.enabledForFeatures);
writeNotNull('creditRateMultiplier', instance.creditRateMultiplier);
writeNotNull('maxConcurrentRequests', instance.maxConcurrentRequests);
writeNotNull('dailyRequestLimit', instance.dailyRequestLimit);
writeNotNull('hourlyRequestLimit', instance.hourlyRequestLimit);
writeNotNull('priority', instance.priority);
writeNotNull('description', instance.description);
writeNotNull('tags', instance.tags);
return val;
}
ApiKeyRequest _$ApiKeyRequestFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'ApiKeyRequest',
json,
($checkedConvert) {
final val = ApiKeyRequest(
apiKey: $checkedConvert('apiKey', (v) => v as String),
note: $checkedConvert('note', (v) => v as String?),
);
return val;
},
);
Map<String, dynamic> _$ApiKeyRequestToJson(ApiKeyRequest instance) {
final val = <String, dynamic>{
'apiKey': instance.apiKey,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('note', instance.note);
return val;
}
PublicModelConfigWithKeys _$PublicModelConfigWithKeysFromJson(
Map<String, dynamic> json) =>
$checkedCreate(
'PublicModelConfigWithKeys',
json,
($checkedConvert) {
final val = PublicModelConfigWithKeys(
id: $checkedConvert('id', (v) => v as String?),
provider: $checkedConvert('provider', (v) => v as String),
modelId: $checkedConvert('modelId', (v) => v as String),
displayName: $checkedConvert('displayName', (v) => v as String?),
enabled: $checkedConvert('enabled', (v) => v as bool?),
apiEndpoint: $checkedConvert('apiEndpoint', (v) => v as String?),
isValidated: $checkedConvert('isValidated', (v) => v as bool?),
apiKeyPoolStatus:
$checkedConvert('apiKeyPoolStatus', (v) => v as String?),
apiKeyStatuses: $checkedConvert(
'apiKeyStatuses',
(v) => (v as List<dynamic>?)
?.map((e) =>
ApiKeyWithStatus.fromJson(e as Map<String, dynamic>))
.toList()),
enabledForFeatures: $checkedConvert('enabledForFeatures',
(v) => PublicModelConfigWithKeys._enabledFeaturesFromJson(v)),
creditRateMultiplier: $checkedConvert(
'creditRateMultiplier', (v) => (v as num?)?.toDouble()),
maxConcurrentRequests: $checkedConvert(
'maxConcurrentRequests', (v) => (v as num?)?.toInt()),
dailyRequestLimit:
$checkedConvert('dailyRequestLimit', (v) => (v as num?)?.toInt()),
hourlyRequestLimit: $checkedConvert(
'hourlyRequestLimit', (v) => (v as num?)?.toInt()),
priority: $checkedConvert('priority', (v) => (v as num?)?.toInt()),
description: $checkedConvert('description', (v) => v as String?),
tags: $checkedConvert('tags',
(v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
createdAt: $checkedConvert(
'createdAt', (v) => PublicModelConfigWithKeys._parseDateTime(v)),
updatedAt: $checkedConvert(
'updatedAt', (v) => PublicModelConfigWithKeys._parseDateTime(v)),
createdBy: $checkedConvert('createdBy', (v) => v as String?),
updatedBy: $checkedConvert('updatedBy', (v) => v as String?),
pricingInfo: $checkedConvert(
'pricingInfo',
(v) => v == null
? null
: PricingInfo.fromJson(v as Map<String, dynamic>)),
usageStatistics: $checkedConvert(
'usageStatistics',
(v) => v == null
? null
: UsageStatistics.fromJson(v as Map<String, dynamic>)),
);
return val;
},
);
Map<String, dynamic> _$PublicModelConfigWithKeysToJson(
PublicModelConfigWithKeys instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('id', instance.id);
val['provider'] = instance.provider;
val['modelId'] = instance.modelId;
writeNotNull('displayName', instance.displayName);
writeNotNull('enabled', instance.enabled);
writeNotNull('apiEndpoint', instance.apiEndpoint);
writeNotNull('isValidated', instance.isValidated);
writeNotNull('apiKeyPoolStatus', instance.apiKeyPoolStatus);
writeNotNull('apiKeyStatuses',
instance.apiKeyStatuses?.map((e) => e.toJson()).toList());
writeNotNull(
'enabledForFeatures',
PublicModelConfigWithKeys._enabledFeaturesToJson(
instance.enabledForFeatures));
writeNotNull('creditRateMultiplier', instance.creditRateMultiplier);
writeNotNull('maxConcurrentRequests', instance.maxConcurrentRequests);
writeNotNull('dailyRequestLimit', instance.dailyRequestLimit);
writeNotNull('hourlyRequestLimit', instance.hourlyRequestLimit);
writeNotNull('priority', instance.priority);
writeNotNull('description', instance.description);
writeNotNull('tags', instance.tags);
writeNotNull('createdAt',
PublicModelConfigWithKeys._dateTimeToJson(instance.createdAt));
writeNotNull('updatedAt',
PublicModelConfigWithKeys._dateTimeToJson(instance.updatedAt));
writeNotNull('createdBy', instance.createdBy);
writeNotNull('updatedBy', instance.updatedBy);
writeNotNull('pricingInfo', instance.pricingInfo?.toJson());
writeNotNull('usageStatistics', instance.usageStatistics?.toJson());
return val;
}
PublicModel _$PublicModelFromJson(Map<String, dynamic> json) => $checkedCreate(
'PublicModel',
json,
($checkedConvert) {
final val = PublicModel(
id: $checkedConvert('id', (v) => v as String),
provider: $checkedConvert('provider', (v) => v as String),
modelId: $checkedConvert('modelId', (v) => v as String),
displayName: $checkedConvert('displayName', (v) => v as String),
description: $checkedConvert('description', (v) => v as String?),
creditRateMultiplier: $checkedConvert(
'creditRateMultiplier', (v) => (v as num?)?.toDouble()),
supportedFeatures: $checkedConvert('supportedFeatures',
(v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
tags: $checkedConvert('tags',
(v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
performanceMetrics: $checkedConvert(
'performanceMetrics',
(v) => v == null
? null
: PerformanceMetrics.fromJson(v as Map<String, dynamic>)),
limitations: $checkedConvert(
'limitations',
(v) => v == null
? null
: LimitationInfo.fromJson(v as Map<String, dynamic>)),
priority: $checkedConvert('priority', (v) => (v as num?)?.toInt()),
recommended: $checkedConvert('recommended', (v) => v as bool?),
);
return val;
},
);
Map<String, dynamic> _$PublicModelToJson(PublicModel instance) {
final val = <String, dynamic>{
'id': instance.id,
'provider': instance.provider,
'modelId': instance.modelId,
'displayName': instance.displayName,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('description', instance.description);
writeNotNull('creditRateMultiplier', instance.creditRateMultiplier);
writeNotNull('supportedFeatures', instance.supportedFeatures);
writeNotNull('tags', instance.tags);
writeNotNull('performanceMetrics', instance.performanceMetrics?.toJson());
writeNotNull('limitations', instance.limitations?.toJson());
writeNotNull('priority', instance.priority);
writeNotNull('recommended', instance.recommended);
return val;
}
PerformanceMetrics _$PerformanceMetricsFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'PerformanceMetrics',
json,
($checkedConvert) {
final val = PerformanceMetrics(
averageResponseTimeMs: $checkedConvert(
'averageResponseTimeMs', (v) => (v as num?)?.toInt()),
throughputPerMinute: $checkedConvert(
'throughputPerMinute', (v) => (v as num?)?.toInt()),
availabilityPercentage: $checkedConvert(
'availabilityPercentage', (v) => (v as num?)?.toDouble()),
qualityScore:
$checkedConvert('qualityScore', (v) => (v as num?)?.toDouble()),
);
return val;
},
);
Map<String, dynamic> _$PerformanceMetricsToJson(PerformanceMetrics instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('averageResponseTimeMs', instance.averageResponseTimeMs);
writeNotNull('throughputPerMinute', instance.throughputPerMinute);
writeNotNull('availabilityPercentage', instance.availabilityPercentage);
writeNotNull('qualityScore', instance.qualityScore);
return val;
}
LimitationInfo _$LimitationInfoFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'LimitationInfo',
json,
($checkedConvert) {
final val = LimitationInfo(
maxContextLength:
$checkedConvert('maxContextLength', (v) => (v as num?)?.toInt()),
requestsPerMinute:
$checkedConvert('requestsPerMinute', (v) => (v as num?)?.toInt()),
requestsPerHour:
$checkedConvert('requestsPerHour', (v) => (v as num?)?.toInt()),
requestsPerDay:
$checkedConvert('requestsPerDay', (v) => (v as num?)?.toInt()),
supportsStreaming:
$checkedConvert('supportsStreaming', (v) => v as bool?),
);
return val;
},
);
Map<String, dynamic> _$LimitationInfoToJson(LimitationInfo instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('maxContextLength', instance.maxContextLength);
writeNotNull('requestsPerMinute', instance.requestsPerMinute);
writeNotNull('requestsPerHour', instance.requestsPerHour);
writeNotNull('requestsPerDay', instance.requestsPerDay);
writeNotNull('supportsStreaming', instance.supportsStreaming);
return val;
}

View File

@@ -0,0 +1,48 @@
/// 修订历史模型
class Revision {
/// 构造函数
Revision({
required this.id,
required this.sceneId,
required this.title,
required this.timestamp,
required this.content,
});
/// 从JSON创建Revision实例
factory Revision.fromJson(Map<String, dynamic> json) {
return Revision(
id: json['id'] as String,
sceneId: json['sceneId'] as String,
title: json['title'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
content: json['content'] as String,
);
}
/// 修订唯一标识符
final String id;
/// 关联的场景ID
final String sceneId;
/// 修订标题
final String title;
/// 修订时间
final DateTime timestamp;
/// 修订内容
final String content;
/// 将Revision实例转换为JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'sceneId': sceneId,
'title': title,
'timestamp': timestamp.toIso8601String(),
'content': content,
};
}
}

View File

@@ -0,0 +1,56 @@
/// 保存设定结果
///
/// 对应后端 SaveSettingResponse 的结构
///
/// 包含保存成功后返回的重要信息
class SaveResult {
/// 保存是否成功
final bool success;
/// 返回消息
final String message;
/// 根设定ID列表
final List<String> rootSettingIds;
/// 自动创建的历史记录ID
final String? historyId;
const SaveResult({
required this.success,
required this.message,
required this.rootSettingIds,
this.historyId,
});
factory SaveResult.fromJson(Map<String, dynamic> json) {
return SaveResult(
success: json['success'] as bool? ?? false,
message: json['message'] as String? ?? '',
rootSettingIds: (json['rootSettingIds'] as List<dynamic>?)?.cast<String>() ?? [],
historyId: json['historyId'] as String?,
);
}
Map<String, dynamic> toJson() => {
'success': success,
'message': message,
'rootSettingIds': rootSettingIds,
if (historyId != null) 'historyId': historyId,
};
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is SaveResult &&
other.success == success &&
other.message == message &&
other.historyId == historyId;
}
@override
int get hashCode => success.hashCode ^ message.hashCode ^ historyId.hashCode;
@override
String toString() => 'SaveResult(success: $success, message: $message, historyId: $historyId)';
}

View File

@@ -0,0 +1,459 @@
import 'dart:convert';
import 'package:ainoval/models/ai_request_models.dart';
import 'package:ainoval/models/context_selection_models.dart';
import 'package:ainoval/utils/logger.dart';
/// 场景节拍组件数据模型
/// 存储在Quill文档中的自包含配置数据
class SceneBeatData {
/// AI请求的完整配置序列化为JSON字符串
/// 这是配置的"快照",包含模型、参数、上下文等所有信息
final String requestData;
/// AI最后生成的内容存储为Quill的Delta JSON字符串
/// 以便在内部的子编辑器中显示富文本
final String generatedContentDelta;
/// (可选) 为了UI方便记录上次加载的预设ID
/// 这样在下次打开编辑弹窗时,可以高亮显示对应的预设
/// **注意此字段仅用于UI展示不参与AI请求逻辑**
final String? lastUsedPresetId;
/// 🚀 新增选中的统一模型ID用于UI状态恢复
final String? selectedUnifiedModelId;
/// 🚀 新增:选中的字数长度('200', '400', '600' 或自定义值)
final String? selectedLength;
/// 🚀 新增温度参数0.0-2.0
final double temperature;
/// 🚀 新增Top-P参数0.0-1.0
final double topP;
/// 🚀 新增:是否启用智能上下文
final bool enableSmartContext;
/// 🚀 新增选中的提示词模板ID
final String? selectedPromptTemplateId;
/// 🚀 新增上下文选择数据序列化为JSON字符串
final String? contextSelectionsData;
/// 组件创建时间
final DateTime createdAt;
/// 组件最后更新时间
final DateTime updatedAt;
/// 组件状态
final SceneBeatStatus status;
/// 生成进度0.0-1.0
final double progress;
SceneBeatData({
required this.requestData,
this.generatedContentDelta = '[{"insert":"\\n"}]', // 默认为空文档
this.lastUsedPresetId,
this.selectedUnifiedModelId,
this.selectedLength,
this.temperature = 0.7,
this.topP = 0.9,
this.enableSmartContext = true,
this.selectedPromptTemplateId,
this.contextSelectionsData,
DateTime? createdAt,
DateTime? updatedAt,
this.status = SceneBeatStatus.draft,
this.progress = 0.0,
}) : createdAt = createdAt ?? DateTime.now(),
updatedAt = updatedAt ?? DateTime.now();
/// 从存储在Quill Delta中的JSON字符串反序列化
factory SceneBeatData.fromJson(String jsonString) {
try {
final map = jsonDecode(jsonString);
return SceneBeatData(
requestData: map['requestData'] as String? ?? '{}',
generatedContentDelta: map['generatedContentDelta'] as String? ?? '[{"insert":"\\n"}]',
lastUsedPresetId: map['lastUsedPresetId'] as String?,
selectedUnifiedModelId: map['selectedUnifiedModelId'] as String?,
selectedLength: map['selectedLength'] as String?,
temperature: (map['temperature'] as num? ?? 0.7).toDouble(),
topP: (map['topP'] as num? ?? 0.9).toDouble(),
enableSmartContext: map['enableSmartContext'] as bool? ?? true,
selectedPromptTemplateId: map['selectedPromptTemplateId'] as String?,
contextSelectionsData: map['contextSelectionsData'] as String?,
createdAt: map['createdAt'] != null
? DateTime.parse(map['createdAt'] as String)
: DateTime.now(),
updatedAt: map['updatedAt'] != null
? DateTime.parse(map['updatedAt'] as String)
: DateTime.now(),
status: SceneBeatStatus.values.firstWhere(
(s) => s.name == (map['status'] as String? ?? 'draft'),
orElse: () => SceneBeatStatus.draft,
),
progress: (map['progress'] as num? ?? 0.0).toDouble(),
);
} catch (e) {
AppLogger.e('SceneBeatData', '解析SceneBeatData失败: $e');
// 如果解析失败,返回一个安全的默认值
return SceneBeatData(
requestData: '{}',
generatedContentDelta: '[{"insert":"\\n"}]',
);
}
}
/// 序列化为JSON字符串以存储在Quill Delta中
String toJson() {
return jsonEncode({
'requestData': requestData,
'generatedContentDelta': generatedContentDelta,
'lastUsedPresetId': lastUsedPresetId,
'selectedUnifiedModelId': selectedUnifiedModelId,
'selectedLength': selectedLength,
'temperature': temperature,
'topP': topP,
'enableSmartContext': enableSmartContext,
'selectedPromptTemplateId': selectedPromptTemplateId,
'contextSelectionsData': contextSelectionsData,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'status': status.name,
'progress': progress,
});
}
/// 一个方便的getter用于获取反序列化后的请求对象
UniversalAIRequest? get parsedRequest {
try {
if (requestData.isEmpty || requestData == '{}') {
return null;
}
final requestJson = jsonDecode(requestData);
// 🚀 兼容性处理:将旧的 NOVEL_GENERATION 类型转换为 SCENE_BEAT_GENERATION
if (requestJson['requestType'] == 'NOVEL_GENERATION' &&
requestJson['metadata'] != null &&
requestJson['metadata']['action'] == 'scene_beat') {
requestJson['requestType'] = 'SCENE_BEAT_GENERATION';
AppLogger.d('SceneBeatData', '自动将旧版场景节拍请求类型更新为 SCENE_BEAT_GENERATION');
}
return UniversalAIRequest.fromJson(requestJson);
} catch (e) {
AppLogger.e('SceneBeatData', '解析UniversalAIRequest失败: $e');
return null;
}
}
/// 更新请求数据
SceneBeatData updateRequestData(UniversalAIRequest request) {
return SceneBeatData(
requestData: jsonEncode(request.toApiJson()),
generatedContentDelta: generatedContentDelta,
lastUsedPresetId: lastUsedPresetId,
selectedUnifiedModelId: selectedUnifiedModelId,
selectedLength: selectedLength,
temperature: temperature,
topP: topP,
enableSmartContext: enableSmartContext,
selectedPromptTemplateId: selectedPromptTemplateId,
contextSelectionsData: contextSelectionsData,
createdAt: createdAt,
updatedAt: DateTime.now(),
status: status,
progress: progress,
);
}
/// 更新生成的内容
SceneBeatData updateGeneratedContent(String deltaJson) {
return SceneBeatData(
requestData: requestData,
generatedContentDelta: deltaJson,
lastUsedPresetId: lastUsedPresetId,
selectedUnifiedModelId: selectedUnifiedModelId,
selectedLength: selectedLength,
temperature: temperature,
topP: topP,
enableSmartContext: enableSmartContext,
selectedPromptTemplateId: selectedPromptTemplateId,
contextSelectionsData: contextSelectionsData,
createdAt: createdAt,
updatedAt: DateTime.now(),
status: status == SceneBeatStatus.draft ? SceneBeatStatus.generated : status,
progress: progress,
);
}
/// 更新状态和进度
SceneBeatData updateStatus(SceneBeatStatus newStatus, {double? newProgress}) {
return SceneBeatData(
requestData: requestData,
generatedContentDelta: generatedContentDelta,
lastUsedPresetId: lastUsedPresetId,
selectedUnifiedModelId: selectedUnifiedModelId,
selectedLength: selectedLength,
temperature: temperature,
topP: topP,
enableSmartContext: enableSmartContext,
selectedPromptTemplateId: selectedPromptTemplateId,
contextSelectionsData: contextSelectionsData,
createdAt: createdAt,
updatedAt: DateTime.now(),
status: newStatus,
progress: newProgress ?? progress,
);
}
/// 复制数据
SceneBeatData copyWith({
String? requestData,
String? generatedContentDelta,
String? lastUsedPresetId,
String? selectedUnifiedModelId,
String? selectedLength,
double? temperature,
double? topP,
bool? enableSmartContext,
String? selectedPromptTemplateId,
String? contextSelectionsData,
DateTime? createdAt,
DateTime? updatedAt,
SceneBeatStatus? status,
double? progress,
}) {
return SceneBeatData(
requestData: requestData ?? this.requestData,
generatedContentDelta: generatedContentDelta ?? this.generatedContentDelta,
lastUsedPresetId: lastUsedPresetId ?? this.lastUsedPresetId,
selectedUnifiedModelId: selectedUnifiedModelId ?? this.selectedUnifiedModelId,
selectedLength: selectedLength ?? this.selectedLength,
temperature: temperature ?? this.temperature,
topP: topP ?? this.topP,
enableSmartContext: enableSmartContext ?? this.enableSmartContext,
selectedPromptTemplateId: selectedPromptTemplateId ?? this.selectedPromptTemplateId,
contextSelectionsData: contextSelectionsData ?? this.contextSelectionsData,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
status: status ?? this.status,
progress: progress ?? this.progress,
);
}
/// 创建默认的场景节拍数据
factory SceneBeatData.createDefault({
required String userId,
required String novelId,
String? initialPrompt,
}) {
// 创建默认的AI请求配置
final defaultRequest = UniversalAIRequest(
requestType: AIRequestType.sceneBeat,
userId: userId,
novelId: novelId,
prompt: initialPrompt ?? '续写故事。',
instructions: '一个关键时刻,重要的事情发生改变,推动故事发展。',
enableSmartContext: true,
parameters: {
'length': '400',
'temperature': 0.7,
'topP': 0.9,
'maxTokens': 4000,
},
metadata: {
'action': 'scene_beat',
'source': 'scene_beat_component',
'featureType': 'SCENE_BEAT_GENERATION',
},
);
return SceneBeatData(
requestData: jsonEncode(defaultRequest.toApiJson()),
generatedContentDelta: '[{"insert":"\\n"}]',
selectedLength: '400',
temperature: 0.7,
topP: 0.9,
enableSmartContext: true,
status: SceneBeatStatus.draft,
progress: 0.0,
);
}
/// 🚀 新增:获取解析后的上下文选择数据
ContextSelectionData? get parsedContextSelections {
if (contextSelectionsData == null || contextSelectionsData!.isEmpty) {
return null;
}
try {
final map = jsonDecode(contextSelectionsData!);
final selectedItems = <String, ContextSelectionItem>{};
final availableItems = <ContextSelectionItem>[];
final flatItems = <String, ContextSelectionItem>{};
// 解析选中的项目
final selectedList = map['selectedItems'] as List<dynamic>? ?? [];
for (final itemData in selectedList) {
final item = ContextSelectionItem(
id: itemData['id'] as String,
title: itemData['title'] as String,
type: ContextSelectionType.values.firstWhere(
(type) => type.value == itemData['type'], // 🚀 修复使用API值而不是displayName
orElse: () => ContextSelectionType.fullNovelText,
),
metadata: Map<String, dynamic>.from(itemData['metadata'] ?? {}),
selectionState: SelectionState.fullySelected,
);
selectedItems[item.id] = item;
availableItems.add(item);
flatItems[item.id] = item;
}
return ContextSelectionData(
novelId: map['novelId'] as String? ?? 'scene_beat',
selectedItems: selectedItems,
availableItems: availableItems,
flatItems: flatItems,
);
} catch (e) {
AppLogger.e('SceneBeatData', '解析上下文选择数据失败: $e');
return null;
}
}
/// 🚀 新增:更新上下文选择数据
SceneBeatData updateContextSelections(ContextSelectionData? contextData) {
String? serializedData;
if (contextData != null && contextData.selectedCount > 0) {
// 序列化选中的项目
final selectedList = contextData.selectedItems.values.map((item) => {
'id': item.id,
'title': item.title,
'type': item.type.value, // 🚀 修复使用API值而不是displayName
'metadata': item.metadata,
}).toList();
serializedData = jsonEncode({
'novelId': contextData.novelId,
'selectedItems': selectedList,
});
}
return copyWith(
contextSelectionsData: serializedData,
updatedAt: DateTime.now(),
);
}
/// 🚀 新增更新UI配置不更新请求数据
SceneBeatData updateUIConfig({
String? selectedUnifiedModelId,
String? selectedLength,
double? temperature,
double? topP,
bool? enableSmartContext,
String? selectedPromptTemplateId,
ContextSelectionData? contextSelections,
}) {
String? serializedContextData = this.contextSelectionsData;
if (contextSelections != null) {
final selectedList = contextSelections.selectedItems.values.map((item) => {
'id': item.id,
'title': item.title,
'type': item.type.value, // 🚀 修复使用API值而不是displayName
'metadata': item.metadata,
}).toList();
serializedContextData = jsonEncode({
'novelId': contextSelections.novelId,
'selectedItems': selectedList,
});
}
return copyWith(
selectedUnifiedModelId: selectedUnifiedModelId,
selectedLength: selectedLength,
temperature: temperature,
topP: topP,
enableSmartContext: enableSmartContext,
selectedPromptTemplateId: selectedPromptTemplateId,
contextSelectionsData: serializedContextData,
updatedAt: DateTime.now(),
);
}
/// 轻量级占位实例:折叠状态下仅存最小信息、避免占用大量内存
/// 注意:当面板真正展开时请调用 `createDefault` 或相应的 update* 方法替换掉该实例
static SceneBeatData get empty => SceneBeatData(requestData: '{}');
}
/// 场景节拍状态枚举
enum SceneBeatStatus {
/// 草稿状态 - 刚创建,还未生成内容
draft,
/// 生成中 - 正在进行AI生成
generating,
/// 已生成 - AI生成完成
generated,
/// 已应用 - 生成的内容已被用户接受并应用
applied,
/// 错误状态 - 生成过程中发生错误
error,
}
extension SceneBeatStatusExtension on SceneBeatStatus {
/// 获取状态的显示名称
String get displayName {
switch (this) {
case SceneBeatStatus.draft:
return '草稿';
case SceneBeatStatus.generating:
return '生成中';
case SceneBeatStatus.generated:
return '已生成';
case SceneBeatStatus.applied:
return '已应用';
case SceneBeatStatus.error:
return '错误';
}
}
/// 获取状态的图标
String get icon {
switch (this) {
case SceneBeatStatus.draft:
return '📝';
case SceneBeatStatus.generating:
return '';
case SceneBeatStatus.generated:
return '';
case SceneBeatStatus.applied:
return '🎯';
case SceneBeatStatus.error:
return '';
}
}
/// 是否可以编辑
bool get canEdit {
return this != SceneBeatStatus.generating;
}
/// 是否可以生成
bool get canGenerate {
return this != SceneBeatStatus.generating;
}
/// 是否可以应用
bool get canApply {
return this == SceneBeatStatus.generated;
}
}

View File

@@ -0,0 +1,108 @@
import 'package:ainoval/utils/logger.dart';
/// 场景摘要DTO
/// 用于服务器返回的场景摘要数据,仅包含场景的基本信息和摘要,不包含完整内容
class SceneSummaryDto {
final String id;
final String novelId;
final String chapterId;
final String title;
final String summary;
final int sequence;
final int wordCount;
final DateTime updatedAt;
SceneSummaryDto({
required this.id,
required this.novelId,
required this.chapterId,
required this.title,
required this.summary,
required this.sequence,
required this.wordCount,
required this.updatedAt,
});
/// 从JSON创建SceneSummaryDto实例
factory SceneSummaryDto.fromJson(Map<String, dynamic> json) {
try {
// 确保必要字段存在,并提供默认值
final String id = json['id'] as String? ?? '';
if (id.isEmpty) {
AppLogger.w('SceneSummaryDto', '场景摘要缺少ID字段');
}
// 解析日期,如果无法解析则使用当前时间
DateTime parsedUpdatedAt;
if (json.containsKey('updatedAt') && json['updatedAt'] is String) {
try {
parsedUpdatedAt = DateTime.parse(json['updatedAt'] as String);
} catch (e) {
AppLogger.w('SceneSummaryDto', '解析updatedAt失败: ${json['updatedAt']},使用当前时间');
parsedUpdatedAt = DateTime.now();
}
} else {
AppLogger.w('SceneSummaryDto', '场景摘要缺少updatedAt字段或格式不正确使用当前时间');
parsedUpdatedAt = DateTime.now();
}
// 处理sequence和wordCount字段
int sequence = 0;
if (json.containsKey('sequence')) {
if (json['sequence'] is int) {
sequence = json['sequence'] as int;
} else if (json['sequence'] is String) {
sequence = int.tryParse(json['sequence'] as String) ?? 0;
}
}
int wordCount = 0;
if (json.containsKey('wordCount')) {
if (json['wordCount'] is int) {
wordCount = json['wordCount'] as int;
} else if (json['wordCount'] is String) {
wordCount = int.tryParse(json['wordCount'] as String) ?? 0;
}
}
return SceneSummaryDto(
id: id,
novelId: json['novelId'] as String? ?? '',
chapterId: json['chapterId'] as String? ?? '',
title: json['title'] as String? ?? '',
summary: json['summary'] as String? ?? '',
sequence: sequence,
wordCount: wordCount,
updatedAt: parsedUpdatedAt,
);
} catch (e) {
AppLogger.e('SceneSummaryDto', '从JSON创建SceneSummaryDto实例失败', e);
// 返回包含默认值的对象,避免崩溃
return SceneSummaryDto(
id: json['id'] as String? ?? 'error_${DateTime.now().millisecondsSinceEpoch}',
novelId: json['novelId'] as String? ?? '',
chapterId: json['chapterId'] as String? ?? '',
title: '解析错误',
summary: '',
sequence: 0,
wordCount: 0,
updatedAt: DateTime.now(),
);
}
}
/// 转换为Map
Map<String, dynamic> toJson() {
return {
'id': id,
'novelId': novelId,
'chapterId': chapterId,
'title': title,
'summary': summary,
'sequence': sequence,
'wordCount': wordCount,
'updatedAt': updatedAt.toIso8601String(),
};
}
}

View File

@@ -0,0 +1,112 @@
/// 场景历史版本条目
class SceneHistoryEntry {
factory SceneHistoryEntry.fromJson(Map<String, dynamic> json) {
return SceneHistoryEntry(
content: json['content'],
updatedAt: DateTime.parse(json['updatedAt']),
updatedBy: json['updatedBy'],
reason: json['reason'],
);
}
SceneHistoryEntry({
this.content,
required this.updatedAt,
required this.updatedBy,
required this.reason,
});
final String? content;
final DateTime updatedAt;
final String updatedBy;
final String reason;
Map<String, dynamic> toJson() => {
'content': content,
'updatedAt': updatedAt.toIso8601String(),
'updatedBy': updatedBy,
'reason': reason,
};
}
/// 场景版本差异
class SceneVersionDiff {
SceneVersionDiff({
required this.originalContent,
required this.newContent,
required this.diff,
});
factory SceneVersionDiff.fromJson(Map<String, dynamic> json) {
return SceneVersionDiff(
originalContent: json['originalContent'] ?? '',
newContent: json['newContent'] ?? '',
diff: json['diff'] ?? '',
);
}
final String originalContent;
final String newContent;
final String diff;
Map<String, dynamic> toJson() => {
'originalContent': originalContent,
'newContent': newContent,
'diff': diff,
};
}
/// 场景内容更新请求
class SceneContentUpdateDto {
SceneContentUpdateDto({
required this.content,
required this.userId,
required this.reason,
});
final String content;
final String userId;
final String reason;
Map<String, dynamic> toJson() => {
'content': content,
'userId': userId,
'reason': reason,
};
}
/// 场景版本恢复请求
class SceneRestoreDto {
SceneRestoreDto({
required this.historyIndex,
required this.userId,
required this.reason,
});
final int historyIndex;
final String userId;
final String reason;
Map<String, dynamic> toJson() => {
'historyIndex': historyIndex,
'userId': userId,
'reason': reason,
};
}
/// 场景版本比较请求
class SceneVersionCompareDto {
SceneVersionCompareDto({
required this.versionIndex1,
required this.versionIndex2,
});
final int versionIndex1;
final int versionIndex2;
Map<String, dynamic> toJson() => {
'versionIndex1': versionIndex1,
'versionIndex2': versionIndex2,
};
}

View File

@@ -0,0 +1,397 @@
import 'setting_node.dart';
import 'package:ainoval/utils/date_time_parser.dart';
/// 设定生成事件基类
abstract class SettingGenerationEvent {
final String sessionId;
final DateTime timestamp;
final String eventType;
const SettingGenerationEvent({
required this.sessionId,
required this.timestamp,
required this.eventType,
});
factory SettingGenerationEvent.fromJson(Map<String, dynamic> json) {
final eventType = json['eventType'] as String;
switch (eventType) {
case 'SESSION_STARTED':
return SessionStartedEvent.fromJson(json);
case 'NODE_CREATED':
return NodeCreatedEvent.fromJson(json);
case 'NODE_UPDATED':
return NodeUpdatedEvent.fromJson(json);
case 'NODE_DELETED':
return NodeDeletedEvent.fromJson(json);
case 'GENERATION_PROGRESS':
return GenerationProgressEvent.fromJson(json);
case 'GENERATION_COMPLETED':
return GenerationCompletedEvent.fromJson(json);
case 'GENERATION_ERROR':
return GenerationErrorEvent.fromJson(json);
case 'COST_ESTIMATION':
return CostEstimationEvent.fromJson(json);
default:
throw ArgumentError('Unknown event type: $eventType');
}
}
Map<String, dynamic> toJson();
}
/// 预计积分事件
class CostEstimationEvent extends SettingGenerationEvent {
final int? estimatedCost;
final int? estimatedInputTokens;
final int? estimatedOutputTokens;
final String? modelProvider;
final String? modelId;
final double? creditMultiplier;
final bool? publicModel;
const CostEstimationEvent({
required String sessionId,
required DateTime timestamp,
this.estimatedCost,
this.estimatedInputTokens,
this.estimatedOutputTokens,
this.modelProvider,
this.modelId,
this.creditMultiplier,
this.publicModel,
}) : super(
sessionId: sessionId,
timestamp: timestamp,
eventType: 'COST_ESTIMATION',
);
factory CostEstimationEvent.fromJson(Map<String, dynamic> json) {
return CostEstimationEvent(
sessionId: json['sessionId'] as String,
timestamp: parseBackendDateTime(json['timestamp']),
estimatedCost: (json['estimatedCost'] as num?)?.toInt(),
estimatedInputTokens: (json['estimatedInputTokens'] as num?)?.toInt(),
estimatedOutputTokens: (json['estimatedOutputTokens'] as num?)?.toInt(),
modelProvider: json['modelProvider'] as String?,
modelId: json['modelId'] as String?,
creditMultiplier: (json['creditMultiplier'] as num?)?.toDouble(),
publicModel: json['publicModel'] as bool?,
);
}
@override
Map<String, dynamic> toJson() => {
'sessionId': sessionId,
'timestamp': timestamp.toIso8601String(),
'eventType': eventType,
'estimatedCost': estimatedCost,
'estimatedInputTokens': estimatedInputTokens,
'estimatedOutputTokens': estimatedOutputTokens,
'modelProvider': modelProvider,
'modelId': modelId,
'creditMultiplier': creditMultiplier,
'publicModel': publicModel,
};
}
/// 会话开始事件
class SessionStartedEvent extends SettingGenerationEvent {
final String initialPrompt;
final String strategy;
const SessionStartedEvent({
required String sessionId,
required DateTime timestamp,
required this.initialPrompt,
required this.strategy,
}) : super(
sessionId: sessionId,
timestamp: timestamp,
eventType: 'SESSION_STARTED',
);
factory SessionStartedEvent.fromJson(Map<String, dynamic> json) {
return SessionStartedEvent(
sessionId: json['sessionId'] as String,
timestamp: parseBackendDateTime(json['timestamp']),
initialPrompt: json['initialPrompt'] as String,
strategy: json['strategy'] as String,
);
}
@override
Map<String, dynamic> toJson() => {
'sessionId': sessionId,
'timestamp': timestamp.toIso8601String(),
'eventType': eventType,
'initialPrompt': initialPrompt,
'strategy': strategy,
};
}
/// 节点创建事件
class NodeCreatedEvent extends SettingGenerationEvent {
final SettingNode node;
final String? parentPath; // 从根节点到父节点的路径
const NodeCreatedEvent({
required String sessionId,
required DateTime timestamp,
required this.node,
this.parentPath,
}) : super(
sessionId: sessionId,
timestamp: timestamp,
eventType: 'NODE_CREATED',
);
factory NodeCreatedEvent.fromJson(Map<String, dynamic> json) {
return NodeCreatedEvent(
sessionId: json['sessionId'] as String,
timestamp: parseBackendDateTime(json['timestamp']),
node: SettingNode.fromJson(json['node'] as Map<String, dynamic>),
parentPath: json['parentPath'] as String?,
);
}
@override
Map<String, dynamic> toJson() => {
'sessionId': sessionId,
'timestamp': timestamp.toIso8601String(),
'eventType': eventType,
'node': node.toJson(),
'parentPath': parentPath,
};
}
/// 节点更新事件
class NodeUpdatedEvent extends SettingGenerationEvent {
final SettingNode node;
final List<String> changedFields;
const NodeUpdatedEvent({
required String sessionId,
required DateTime timestamp,
required this.node,
required this.changedFields,
}) : super(
sessionId: sessionId,
timestamp: timestamp,
eventType: 'NODE_UPDATED',
);
factory NodeUpdatedEvent.fromJson(Map<String, dynamic> json) {
return NodeUpdatedEvent(
sessionId: json['sessionId'] as String,
timestamp: parseBackendDateTime(json['timestamp']),
node: SettingNode.fromJson(json['node'] as Map<String, dynamic>),
changedFields: List<String>.from(json['changedFields'] ?? []),
);
}
@override
Map<String, dynamic> toJson() => {
'sessionId': sessionId,
'timestamp': timestamp.toIso8601String(),
'eventType': eventType,
'node': node.toJson(),
'changedFields': changedFields,
};
}
/// 节点删除事件
class NodeDeletedEvent extends SettingGenerationEvent {
final List<String> deletedNodeIds;
final String? reason;
const NodeDeletedEvent({
required String sessionId,
required DateTime timestamp,
required this.deletedNodeIds,
this.reason,
}) : super(
sessionId: sessionId,
timestamp: timestamp,
eventType: 'NODE_DELETED',
);
factory NodeDeletedEvent.fromJson(Map<String, dynamic> json) {
// 兼容旧的 'nodeId' 字段和新的 'deletedNodeIds' 字段
List<String> ids = [];
if (json.containsKey('deletedNodeIds')) {
ids = List<String>.from(json['deletedNodeIds']);
} else if (json.containsKey('nodeId')) {
ids = [json['nodeId'] as String];
}
return NodeDeletedEvent(
sessionId: json['sessionId'] as String,
timestamp: parseBackendDateTime(json['timestamp']),
deletedNodeIds: ids,
reason: json['reason'] as String?,
);
}
@override
Map<String, dynamic> toJson() => {
'sessionId': sessionId,
'timestamp': timestamp.toIso8601String(),
'eventType': eventType,
'deletedNodeIds': deletedNodeIds,
'reason': reason,
};
}
/// 生成进度事件
class GenerationProgressEvent extends SettingGenerationEvent {
final String stage;
final String message;
final int? currentStep;
final int? totalSteps;
final String? nodeId;
const GenerationProgressEvent({
required String sessionId,
required DateTime timestamp,
required this.stage,
required this.message,
this.currentStep,
this.totalSteps,
this.nodeId,
}) : super(
sessionId: sessionId,
timestamp: timestamp,
eventType: 'GENERATION_PROGRESS',
);
factory GenerationProgressEvent.fromJson(Map<String, dynamic> json) {
return GenerationProgressEvent(
sessionId: json['sessionId'] as String,
timestamp: parseBackendDateTime(json['timestamp']),
stage: json['stage'] as String,
message: json['message'] as String,
currentStep: json['currentStep'] as int?,
totalSteps: json['totalSteps'] as int?,
nodeId: json['nodeId'] as String?,
);
}
@override
Map<String, dynamic> toJson() => {
'sessionId': sessionId,
'timestamp': timestamp.toIso8601String(),
'eventType': eventType,
'stage': stage,
'message': message,
'currentStep': currentStep,
'totalSteps': totalSteps,
'nodeId': nodeId,
};
}
/// 生成完成事件
class GenerationCompletedEvent extends SettingGenerationEvent {
final String stage;
final String message;
final String? resultSummary;
final List<String>? affectedNodeIds;
const GenerationCompletedEvent({
required String sessionId,
required DateTime timestamp,
required this.stage,
required this.message,
this.resultSummary,
this.affectedNodeIds,
}) : super(
sessionId: sessionId,
timestamp: timestamp,
eventType: 'GENERATION_COMPLETED',
);
factory GenerationCompletedEvent.fromJson(Map<String, dynamic> json) {
return GenerationCompletedEvent(
sessionId: json['sessionId'] as String,
timestamp: parseBackendDateTime(json['timestamp']),
stage: json['stage'] as String? ?? 'completed',
message: json['message'] as String? ?? '生成完成',
resultSummary: json['resultSummary'] as String?,
affectedNodeIds: json['affectedNodeIds'] != null
? List<String>.from(json['affectedNodeIds'])
: null,
);
}
@override
Map<String, dynamic> toJson() => {
'sessionId': sessionId,
'timestamp': timestamp.toIso8601String(),
'eventType': eventType,
'stage': stage,
'message': message,
'resultSummary': resultSummary,
'affectedNodeIds': affectedNodeIds,
};
}
/// 生成错误事件
class GenerationErrorEvent extends SettingGenerationEvent {
final String errorCode;
final String errorMessage;
final String? stage;
final String? nodeId;
final bool? recoverable;
final String? suggestionForUser;
const GenerationErrorEvent({
required String sessionId,
required DateTime timestamp,
required this.errorCode,
required this.errorMessage,
this.stage,
this.nodeId,
this.recoverable,
this.suggestionForUser,
}) : super(
sessionId: sessionId,
timestamp: timestamp,
eventType: 'GENERATION_ERROR',
);
factory GenerationErrorEvent.fromJson(Map<String, dynamic> json) {
// 兼容后端在 onErrorResume 分支可能未填充的字段
final rawSessionId = json['sessionId'];
final sessionId = (rawSessionId is String && rawSessionId.isNotEmpty)
? rawSessionId
: 'unknown-session';
final rawTimestamp = json['timestamp'];
final timestamp = rawTimestamp != null
? parseBackendDateTime(rawTimestamp)
: DateTime.now();
return GenerationErrorEvent(
sessionId: sessionId,
timestamp: timestamp,
errorCode: (json['errorCode'] as String?) ?? 'UNKNOWN_ERROR',
errorMessage: (json['errorMessage'] as String?) ?? '发生错误',
stage: json['stage'] as String?,
nodeId: json['nodeId'] as String?,
recoverable: json['recoverable'] as bool?,
suggestionForUser: json['suggestionForUser'] as String?,
);
}
@override
Map<String, dynamic> toJson() => {
'sessionId': sessionId,
'timestamp': timestamp.toIso8601String(),
'eventType': eventType,
'errorCode': errorCode,
'errorMessage': errorMessage,
'stage': stage,
'nodeId': nodeId,
'recoverable': recoverable,
'suggestionForUser': suggestionForUser,
};
}

View File

@@ -0,0 +1,346 @@
import 'dart:convert';
import 'setting_node.dart';
import '../utils/date_time_parser.dart';
/// 设定生成会话
class SettingGenerationSession {
final String sessionId;
final String userId;
final String? novelId;
final String initialPrompt;
final String strategy;
final String? modelConfigId;
final SessionStatus status;
final List<SettingNode> rootNodes;
final Map<String, SettingNode> allNodes;
final DateTime createdAt;
final DateTime? updatedAt;
final String? errorMessage;
final Map<String, dynamic> metadata;
final String? historyId; // 新增关联的历史记录ID
const SettingGenerationSession({
required this.sessionId,
required this.userId,
this.novelId,
required this.initialPrompt,
required this.strategy,
this.modelConfigId,
required this.status,
this.rootNodes = const [],
this.allNodes = const {},
required this.createdAt,
this.updatedAt,
this.errorMessage,
this.metadata = const {},
this.historyId, // 新增历史记录ID参数
});
factory SettingGenerationSession.fromJson(Map<String, dynamic> json) {
// 🔧 解析树形结构的rootNodes
List<SettingNode> rootNodes = [];
// 方式1直接从rootNodes字段解析新格式
if (json['rootNodes'] != null && json['rootNodes'] is List && (json['rootNodes'] as List).isNotEmpty) {
rootNodes = (json['rootNodes'] as List)
.map((node) => SettingNode.fromJson(node as Map<String, dynamic>))
.toList();
}
// 方式2从settings数组构建树形结构兼容格式
else if (json['settings'] != null && json['settings'] is List) {
rootNodes = _buildRootNodesFromSettings(json);
}
// 方式3兼容旧格式的rootNodes解析
else if (json['rootNodes'] != null && json['rootNodes'] is List) {
rootNodes = (json['rootNodes'] as List)
.map((node) => SettingNode.fromJson(node as Map<String, dynamic>))
.toList();
}
// 兼容后端大写状态与CANCELLED状态
SessionStatus parseStatus(dynamic raw) {
if (raw == null) return SessionStatus.initializing;
final statusStr = raw.toString().trim();
final lower = statusStr.toLowerCase();
switch (lower) {
case 'initializing':
return SessionStatus.initializing;
case 'generating':
return SessionStatus.generating;
case 'completed':
return SessionStatus.completed;
case 'error':
return SessionStatus.error;
case 'saved':
return SessionStatus.saved;
case 'cancelled':
// 前端未定义cancelled兼容为错误状态显示
return SessionStatus.error;
default:
// 兼容后端返回大写枚举,如 "COMPLETED"、"SAVED" 等
if (statusStr == statusStr.toUpperCase()) {
switch (statusStr) {
case 'INITIALIZING':
return SessionStatus.initializing;
case 'GENERATING':
return SessionStatus.generating;
case 'COMPLETED':
return SessionStatus.completed;
case 'ERROR':
return SessionStatus.error;
case 'SAVED':
return SessionStatus.saved;
case 'CANCELLED':
return SessionStatus.error;
}
}
return SessionStatus.initializing;
}
}
return SettingGenerationSession(
sessionId: json['sessionId'] as String,
userId: json['userId'] as String,
novelId: json['novelId'] as String?,
initialPrompt: json['initialPrompt'] as String,
strategy: json['strategy'] as String,
modelConfigId: json['modelConfigId'] as String?,
status: parseStatus(json['status']),
rootNodes: rootNodes,
allNodes: json['allNodes'] != null
? Map<String, SettingNode>.fromEntries(
(json['allNodes'] as Map<String, dynamic>).entries.map(
(entry) => MapEntry(
entry.key,
SettingNode.fromJson(entry.value as Map<String, dynamic>),
),
),
)
: {},
createdAt: parseBackendDateTime(json['createdAt']),
updatedAt: json['updatedAt'] != null
? parseBackendDateTime(json['updatedAt'])
: null,
errorMessage: json['errorMessage'] as String?,
metadata: Map<String, dynamic>.from(json['metadata'] ?? {}),
historyId: json['historyId'] as String?, // 新增从JSON解析historyId
);
}
Map<String, dynamic> toJson() => {
'sessionId': sessionId,
'userId': userId,
'novelId': novelId,
'initialPrompt': initialPrompt,
'strategy': strategy,
'modelConfigId': modelConfigId,
'status': status.toString().split('.').last,
'rootNodes': rootNodes.map((node) => node.toJson()).toList(),
'allNodes': allNodes.map((key, value) => MapEntry(key, value.toJson())),
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt?.toIso8601String(),
'errorMessage': errorMessage,
'metadata': metadata,
'historyId': historyId, // 新增序列化historyId
};
/// 从settings数组构建rootNodes树形结构
static List<SettingNode> _buildRootNodesFromSettings(Map<String, dynamic> json) {
List<SettingNode> rootNodes = [];
try {
final settings = json['settings'] as List?;
final rootSettingIds = json['rootSettingIds'] as List?;
final parentChildMap = json['parentChildMap'] as Map<String, dynamic>?;
if (settings == null || settings.isEmpty) {
return rootNodes;
}
// 将所有设定转换为SettingNode并建立索引
Map<String, SettingNode> nodeMap = {};
for (var settingData in settings) {
if (settingData is Map<String, dynamic>) {
var node = SettingNode.fromJson(settingData);
nodeMap[node.id] = node;
}
}
// 🔧 方式1优先使用rootSettingIds
if (rootSettingIds != null && rootSettingIds.isNotEmpty) {
for (var rootId in rootSettingIds) {
if (rootId is String && nodeMap.containsKey(rootId)) {
var rootNode = nodeMap[rootId]!;
// 构建这个根节点的完整子树
var treeNode = _buildNodeTree(rootNode, nodeMap, parentChildMap);
rootNodes.add(treeNode);
}
}
}
// 🔧 方式2查找parentId为null的节点
else {
for (var node in nodeMap.values) {
if (node.parentId == null) {
var treeNode = _buildNodeTree(node, nodeMap, parentChildMap);
rootNodes.add(treeNode);
}
}
}
} catch (e) {
print('解析settings构建树形结构失败: $e');
}
return rootNodes;
}
/// 递归构建节点树
static SettingNode _buildNodeTree(
SettingNode parentNode,
Map<String, SettingNode> nodeMap,
Map<String, dynamic>? parentChildMap
) {
List<SettingNode> children = [];
// 🔧 方式1从parentChildMap获取子节点ID列表
if (parentChildMap != null && parentChildMap.containsKey(parentNode.id)) {
var childIds = parentChildMap[parentNode.id] as List?;
if (childIds != null) {
for (var childId in childIds) {
if (childId is String && nodeMap.containsKey(childId)) {
var childNode = nodeMap[childId]!;
var treeChild = _buildNodeTree(childNode, nodeMap, parentChildMap);
children.add(treeChild);
}
}
}
}
// 🔧 方式2从所有节点中查找parentId指向当前节点的子节点
else {
for (var node in nodeMap.values) {
if (node.parentId == parentNode.id) {
var treeChild = _buildNodeTree(node, nodeMap, parentChildMap);
children.add(treeChild);
}
}
}
// 返回包含子节点的节点副本
return parentNode.copyWith(children: children);
}
SettingGenerationSession copyWith({
String? sessionId,
String? userId,
String? novelId,
String? initialPrompt,
String? strategy,
String? modelConfigId,
SessionStatus? status,
List<SettingNode>? rootNodes,
Map<String, SettingNode>? allNodes,
DateTime? createdAt,
DateTime? updatedAt,
String? errorMessage,
Map<String, dynamic>? metadata,
String? historyId, // 新增historyId参数
}) {
return SettingGenerationSession(
sessionId: sessionId ?? this.sessionId,
userId: userId ?? this.userId,
novelId: novelId ?? this.novelId,
initialPrompt: initialPrompt ?? this.initialPrompt,
strategy: strategy ?? this.strategy,
modelConfigId: modelConfigId ?? this.modelConfigId,
status: status ?? this.status,
rootNodes: rootNodes ?? this.rootNodes,
allNodes: allNodes ?? this.allNodes,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
errorMessage: errorMessage ?? this.errorMessage,
metadata: metadata ?? this.metadata,
historyId: historyId ?? this.historyId, // 新增设置historyId
);
}
@override
String toString() {
return jsonEncode(toJson());
}
}
/// 会话状态
enum SessionStatus {
/// 初始化
initializing,
/// 生成中
generating,
/// 已完成
completed,
/// 已错误
error,
/// 已保存
saved,
}
/// 生成策略信息
class StrategyInfo {
final String id;
final String name;
final String description;
final bool enabled;
final Map<String, dynamic> parameters;
final int? expectedRootNodeCount;
final int? maxDepth;
const StrategyInfo({
required this.id,
required this.name,
required this.description,
this.enabled = true,
this.parameters = const {},
this.expectedRootNodeCount,
this.maxDepth,
});
factory StrategyInfo.fromJson(Map<String, dynamic> json) {
// 后端返回的格式:{name, description, expectedRootNodeCount, maxDepth}
// 前端需要生成id字段
String id;
String name;
String description;
if (json.containsKey('id')) {
// 如果已有id字段直接使用
id = json['id'] as String;
name = json['name'] as String;
description = json['description'] as String;
} else {
// 根据后端格式解析
name = json['name'] as String;
description = json['description'] as String;
// 生成ID将名称转换为小写并替换空格为横线
id = name.toLowerCase().replaceAll(' ', '-').replaceAll(' ', '-');
}
return StrategyInfo(
id: id,
name: name,
description: description,
enabled: json['enabled'] as bool? ?? true,
parameters: Map<String, dynamic>.from(json['parameters'] ?? {}),
expectedRootNodeCount: json['expectedRootNodeCount'] as int?,
maxDepth: json['maxDepth'] as int?,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'description': description,
'enabled': enabled,
'parameters': parameters,
if (expectedRootNodeCount != null) 'expectedRootNodeCount': expectedRootNodeCount,
if (maxDepth != null) 'maxDepth': maxDepth,
};
}

View File

@@ -0,0 +1,82 @@
import 'dart:convert';
/// 设定组模型
class SettingGroup {
final String? id;
final String? novelId;
final String? userId;
final String name;
final String? description;
final bool? isActiveContext;
final List<String>? itemIds;
final DateTime? createdAt;
final DateTime? updatedAt;
SettingGroup({
this.id,
this.novelId,
this.userId,
required this.name,
this.description,
this.isActiveContext,
this.itemIds,
this.createdAt,
this.updatedAt,
});
factory SettingGroup.fromJson(Map<String, dynamic> json) {
List<String>? itemIds;
if (json['itemIds'] != null) {
itemIds = List<String>.from(json['itemIds']);
}
dynamic createdAtJson = json['createdAt'];
String? createdAtString;
if (createdAtJson is String) {
createdAtString = createdAtJson;
} else if (createdAtJson is List && createdAtJson.isNotEmpty && createdAtJson.first is String) {
createdAtString = createdAtJson.first;
}
dynamic updatedAtJson = json['updatedAt'];
String? updatedAtString;
if (updatedAtJson is String) {
updatedAtString = updatedAtJson;
} else if (updatedAtJson is List && updatedAtJson.isNotEmpty && updatedAtJson.first is String) {
updatedAtString = updatedAtJson.first;
}
return SettingGroup(
id: json['id'],
novelId: json['novelId'],
userId: json['userId'],
name: json['name'],
description: json['description'],
isActiveContext: json['isActiveContext'],
itemIds: itemIds,
createdAt: createdAtString != null
? DateTime.parse(createdAtString)
: null,
updatedAt: updatedAtString != null
? DateTime.parse(updatedAtString)
: null,
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (id != null) data['id'] = id;
if (novelId != null) data['novelId'] = novelId;
if (userId != null) data['userId'] = userId;
data['name'] = name;
if (description != null) data['description'] = description;
if (isActiveContext != null) data['isActiveContext'] = isActiveContext;
if (itemIds != null) data['itemIds'] = itemIds;
return data;
}
@override
String toString() {
return jsonEncode(toJson());
}
}

View File

@@ -0,0 +1,117 @@
import 'dart:convert';
import 'setting_type.dart';
/// 设定节点
class SettingNode {
final String id;
final String? parentId;
final String name;
final SettingType type;
final String description;
final Map<String, dynamic> attributes;
final Map<String, dynamic> strategyMetadata;
final GenerationStatus generationStatus;
final String? errorMessage;
final String? generationPrompt;
final List<SettingNode>? children;
const SettingNode({
required this.id,
this.parentId,
required this.name,
required this.type,
required this.description,
this.attributes = const {},
this.strategyMetadata = const {},
this.generationStatus = GenerationStatus.pending,
this.errorMessage,
this.generationPrompt,
this.children,
});
factory SettingNode.fromJson(Map<String, dynamic> json) {
return SettingNode(
id: json['id'] as String,
parentId: json['parentId'] as String?,
name: json['name'] as String,
type: SettingType.fromJson(json['type']),
description: json['description'] as String,
attributes: Map<String, dynamic>.from(json['attributes'] ?? {}),
strategyMetadata: Map<String, dynamic>.from(json['strategyMetadata'] ?? {}),
generationStatus: GenerationStatus.values.firstWhere(
(e) => e.toString().split('.').last == json['generationStatus'],
orElse: () => GenerationStatus.pending,
),
errorMessage: json['errorMessage'] as String?,
generationPrompt: json['generationPrompt'] as String?,
children: json['children'] != null
? (json['children'] as List)
.map((child) => SettingNode.fromJson(child as Map<String, dynamic>))
.toList()
: null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'parentId': parentId,
'name': name,
'type': type.toJson(),
'description': description,
'attributes': attributes,
'strategyMetadata': strategyMetadata,
'generationStatus': generationStatus.toString().split('.').last,
'errorMessage': errorMessage,
'generationPrompt': generationPrompt,
'children': children?.map((child) => child.toJson()).toList(),
};
}
SettingNode copyWith({
String? id,
String? parentId,
String? name,
SettingType? type,
String? description,
Map<String, dynamic>? attributes,
Map<String, dynamic>? strategyMetadata,
GenerationStatus? generationStatus,
String? errorMessage,
String? generationPrompt,
List<SettingNode>? children,
}) {
return SettingNode(
id: id ?? this.id,
parentId: parentId ?? this.parentId,
name: name ?? this.name,
type: type ?? this.type,
description: description ?? this.description,
attributes: attributes ?? this.attributes,
strategyMetadata: strategyMetadata ?? this.strategyMetadata,
generationStatus: generationStatus ?? this.generationStatus,
errorMessage: errorMessage ?? this.errorMessage,
generationPrompt: generationPrompt ?? this.generationPrompt,
children: children ?? this.children,
);
}
@override
String toString() {
return jsonEncode(toJson());
}
}
/// 生成状态枚举
enum GenerationStatus {
/// 待生成
pending,
/// 生成中
generating,
/// 已完成
completed,
/// 生成失败
failed,
/// 已修改
modified,
}

View File

@@ -0,0 +1,99 @@
/// 设定关系类型枚举
/// 保留现有的关系系统,但重点突出父子关系
enum SettingRelationshipType {
// 核心:父子关系(最重要)
parent('parent', '父设定'),
child('child', '子设定'),
// 常用关系
friend('friend', '朋友'),
enemy('enemy', '敌人'),
ally('ally', '盟友'),
rival('rival', '竞争对手'),
// 归属关系
owns('owns', '拥有'),
ownedBy('ownedBy', '被拥有'),
memberOf('memberOf', '成员'),
// 地理关系
contains('contains', '包含'),
containedBy('containedBy', '被包含'),
adjacent('adjacent', '相邻'),
// 其他关系
uses('uses', '使用'),
usedBy('usedBy', '被使用'),
related('related', '相关'),
// 自定义关系
custom('custom', '自定义');
const SettingRelationshipType(this.value, this.displayName);
final String value;
final String displayName;
/// 根据值获取枚举
static SettingRelationshipType fromValue(String value) {
return values.firstWhere(
(type) => type.value == value,
orElse: () => custom,
);
}
/// 获取关系类型的反向关系
SettingRelationshipType get inverse {
switch (this) {
case parent:
return child;
case child:
return parent;
case contains:
return containedBy;
case containedBy:
return contains;
case owns:
return ownedBy;
case ownedBy:
return owns;
case uses:
return usedBy;
case usedBy:
return uses;
default:
return this; // 对称关系或自定义关系返回自身
}
}
/// 判断是否为父子关系
bool get isHierarchical {
return this == parent || this == child;
}
/// 判断是否为对称关系(双向相同)
bool get isSymmetric {
const symmetricTypes = {
friend,
enemy,
ally,
rival,
adjacent,
related,
custom,
};
return symmetricTypes.contains(this);
}
/// 按类别分组
static Map<String, List<SettingRelationshipType>> get groupedTypes {
return {
'层级关系': [parent, child],
'社会关系': [friend, enemy, ally, rival],
'归属关系': [owns, ownedBy, memberOf],
'地理关系': [contains, containedBy, adjacent],
'功能关系': [uses, usedBy, related],
'其他': [custom],
};
}
}

View File

@@ -0,0 +1,70 @@
// AINoval/lib/models/setting_type.dart
enum SettingType {
character('CHARACTER', '角色'),
location('LOCATION', '地点'),
item('ITEM', '物品'),
lore('LORE', '背景知识'),
faction('FACTION', '组织/势力'),
event('EVENT', '事件'),
concept('CONCEPT', '概念/规则'),
creature('CREATURE', '生物/种族'),
magicSystem('MAGIC_SYSTEM', '魔法体系'),
technology('TECHNOLOGY', '科技设定'),
culture('CULTURE', '文化'),
history('HISTORY', '历史'),
organization('ORGANIZATION', '组织'),
// —— 通用叙事/世界构建扩展 ——
worldview('WORLDVIEW', '世界观'),
pleasurePoint('PLEASURE_POINT', '爽点'),
anticipationHook('ANTICIPATION_HOOK', '期待感钩子'),
theme('THEME', '主题'),
tone('TONE', '基调'),
style('STYLE', '文风'),
trope('TROPE', '母题/套路'),
plotDevice('PLOT_DEVICE', '剧情装置'),
powerSystem('POWER_SYSTEM', '力量体系'),
goldenFinger('GOLDEN_FINGER', '金手指'),
timeline('TIMELINE', '时间线'),
religion('RELIGION', '宗教'),
politics('POLITICS', '政治'),
economy('ECONOMY', '经济'),
geography('GEOGRAPHY', '地理'),
other('OTHER', '其他');
const SettingType(this.value, this.displayName);
final String value;
final String displayName;
// 为了向后兼容,添加 key 属性
String get key => value;
static SettingType fromValue(String value) {
return SettingType.values.firstWhere(
(e) => e.value == value.toUpperCase(),
orElse: () => SettingType.other,
);
}
// 添加 JSON 序列化支持
static SettingType fromJson(dynamic json) {
if (json is String) {
return fromValue(json);
} else if (json is Map<String, dynamic>) {
return fromValue(json['value'] ?? json['key'] ?? 'OTHER');
}
return SettingType.other;
}
Map<String, dynamic> toJson() => {
'value': value,
'displayName': displayName,
};
}
// Helper for UI if needed
class SettingTypeOption {
final SettingType type;
bool isSelected;
SettingTypeOption(this.type, {this.isSelected = false});
}

View File

@@ -0,0 +1,240 @@
import '../utils/date_time_parser.dart';
/// 策略响应模型
/// 统一处理策略管理API返回的数据结构
class StrategyResponse {
final String id;
final String name;
final String description;
final String? authorId;
final String? authorName;
final bool isPublic;
final DateTime createdAt;
final DateTime? updatedAt;
final int usageCount;
final int expectedRootNodes;
final int maxDepth;
final String reviewStatus;
final List<String> categories;
final List<String> tags;
final int difficultyLevel;
final String? systemPrompt;
final String? userPrompt;
final List<Map<String, dynamic>>? nodeTemplates;
const StrategyResponse({
required this.id,
required this.name,
required this.description,
this.authorId,
this.authorName,
required this.isPublic,
required this.createdAt,
this.updatedAt,
required this.usageCount,
required this.expectedRootNodes,
required this.maxDepth,
required this.reviewStatus,
this.categories = const [],
this.tags = const [],
required this.difficultyLevel,
this.systemPrompt,
this.userPrompt,
this.nodeTemplates,
});
factory StrategyResponse.fromJson(Map<String, dynamic> json) {
return StrategyResponse(
id: json['id'] as String,
name: json['name'] as String,
description: json['description'] as String,
authorId: json['authorId'] as String?,
authorName: json['authorName'] as String?,
isPublic: json['isPublic'] as bool? ?? false,
createdAt: parseBackendDateTime(json['createdAt']),
updatedAt: parseBackendDateTimeSafely(json['updatedAt']),
usageCount: (json['usageCount'] as num?)?.toInt() ?? 0,
expectedRootNodes: (json['expectedRootNodes'] as num?)?.toInt() ?? 0,
maxDepth: (json['maxDepth'] as num?)?.toInt() ?? 5,
reviewStatus: json['reviewStatus'] as String? ?? 'DRAFT',
categories: List<String>.from(json['categories'] ?? []),
tags: List<String>.from(json['tags'] ?? []),
difficultyLevel: (json['difficultyLevel'] as num?)?.toInt() ?? 3,
systemPrompt: json['systemPrompt'] as String?,
userPrompt: json['userPrompt'] as String?,
nodeTemplates: json['nodeTemplates'] != null
? List<Map<String, dynamic>>.from(json['nodeTemplates'])
: null,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'description': description,
'authorId': authorId,
'authorName': authorName,
'isPublic': isPublic,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt?.toIso8601String(),
'usageCount': usageCount,
'expectedRootNodes': expectedRootNodes,
'maxDepth': maxDepth,
'reviewStatus': reviewStatus,
'categories': categories,
'tags': tags,
'difficultyLevel': difficultyLevel,
if (systemPrompt != null) 'systemPrompt': systemPrompt,
if (userPrompt != null) 'userPrompt': userPrompt,
if (nodeTemplates != null) 'nodeTemplates': nodeTemplates,
};
/// 判断是否为系统预设策略
bool get isSystemStrategy => authorId == null || authorId!.isEmpty;
/// 判断是否可以编辑(只有自己创建的策略才能编辑)
bool canEdit(String? currentUserId) {
return !isSystemStrategy && authorId == currentUserId;
}
/// 判断是否可以删除
bool canDelete(String? currentUserId) {
return canEdit(currentUserId);
}
/// 获取策略状态的本地化文本
String get localizedReviewStatus {
switch (reviewStatus) {
case 'DRAFT':
return '草稿';
case 'PENDING':
return '待审核';
case 'APPROVED':
return '已通过';
case 'REJECTED':
return '已拒绝';
default:
return reviewStatus;
}
}
/// 判断策略是否可以提交审核
bool get canSubmitForReview {
return reviewStatus == 'DRAFT' || reviewStatus == 'REJECTED';
}
/// 判断策略是否正在审核中
bool get isPendingReview {
return reviewStatus == 'PENDING';
}
/// 判断策略是否已通过审核
bool get isApproved {
return reviewStatus == 'APPROVED';
}
StrategyResponse copyWith({
String? id,
String? name,
String? description,
String? authorId,
String? authorName,
bool? isPublic,
DateTime? createdAt,
DateTime? updatedAt,
int? usageCount,
int? expectedRootNodes,
int? maxDepth,
String? reviewStatus,
List<String>? categories,
List<String>? tags,
int? difficultyLevel,
String? systemPrompt,
String? userPrompt,
List<Map<String, dynamic>>? nodeTemplates,
}) {
return StrategyResponse(
id: id ?? this.id,
name: name ?? this.name,
description: description ?? this.description,
authorId: authorId ?? this.authorId,
authorName: authorName ?? this.authorName,
isPublic: isPublic ?? this.isPublic,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
usageCount: usageCount ?? this.usageCount,
expectedRootNodes: expectedRootNodes ?? this.expectedRootNodes,
maxDepth: maxDepth ?? this.maxDepth,
reviewStatus: reviewStatus ?? this.reviewStatus,
categories: categories ?? this.categories,
tags: tags ?? this.tags,
difficultyLevel: difficultyLevel ?? this.difficultyLevel,
systemPrompt: systemPrompt ?? this.systemPrompt,
userPrompt: userPrompt ?? this.userPrompt,
nodeTemplates: nodeTemplates ?? this.nodeTemplates,
);
}
}
/// 节点模板配置模型
class NodeTemplateConfig {
final String id;
final String name;
final String type;
final String description;
final int minChildren;
final int maxChildren;
final int minDescriptionLength;
final int maxDescriptionLength;
final bool isRootTemplate;
final int priority;
final String? generationHint;
final List<String> tags;
const NodeTemplateConfig({
required this.id,
required this.name,
required this.type,
required this.description,
this.minChildren = 0,
this.maxChildren = -1,
this.minDescriptionLength = 50,
this.maxDescriptionLength = 500,
this.isRootTemplate = false,
this.priority = 0,
this.generationHint,
this.tags = const [],
});
factory NodeTemplateConfig.fromJson(Map<String, dynamic> json) {
return NodeTemplateConfig(
id: json['id'] as String,
name: json['name'] as String,
type: json['type'] as String,
description: json['description'] as String,
minChildren: (json['minChildren'] as num?)?.toInt() ?? 0,
maxChildren: (json['maxChildren'] as num?)?.toInt() ?? -1,
minDescriptionLength: (json['minDescriptionLength'] as num?)?.toInt() ?? 50,
maxDescriptionLength: (json['maxDescriptionLength'] as num?)?.toInt() ?? 500,
isRootTemplate: json['isRootTemplate'] as bool? ?? false,
priority: (json['priority'] as num?)?.toInt() ?? 0,
generationHint: json['generationHint'] as String?,
tags: List<String>.from(json['tags'] ?? []),
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'type': type,
'description': description,
'minChildren': minChildren,
'maxChildren': maxChildren,
'minDescriptionLength': minDescriptionLength,
'maxDescriptionLength': maxDescriptionLength,
'isRootTemplate': isRootTemplate,
'priority': priority,
if (generationHint != null) 'generationHint': generationHint,
'tags': tags,
};
}

View File

@@ -0,0 +1,86 @@
/// 策略模板信息
///
/// 对应后端 ISettingGenerationService.StrategyTemplateInfo DTO
///
/// 用于替代旧的 StrategyInfo与新的后端API完全对齐
class StrategyTemplateInfo {
/// 策略模板IDpromptTemplateId
final String promptTemplateId;
/// 策略名称
final String name;
/// 策略描述
final String description;
/// 分类列表
final List<String> categories;
/// 标签列表
final List<String> tags;
/// 预期根节点数量
final int? expectedRootNodes;
/// 最大深度
final int? maxDepth;
/// 难度等级
final int? difficultyLevel;
/// 是否启用
final bool enabled;
const StrategyTemplateInfo({
required this.promptTemplateId,
required this.name,
required this.description,
this.categories = const [],
this.tags = const [],
this.expectedRootNodes,
this.maxDepth,
this.difficultyLevel,
this.enabled = true,
});
factory StrategyTemplateInfo.fromJson(Map<String, dynamic> json) {
return StrategyTemplateInfo(
promptTemplateId: json['promptTemplateId'] as String,
name: json['name'] as String,
description: json['description'] as String,
categories: (json['categories'] as List<dynamic>?)?.cast<String>() ?? [],
tags: (json['tags'] as List<dynamic>?)?.cast<String>() ?? [],
expectedRootNodes: json['expectedRootNodes'] as int?,
maxDepth: json['maxDepth'] as int?,
difficultyLevel: json['difficultyLevel'] as int?,
enabled: json['enabled'] as bool? ?? true,
);
}
Map<String, dynamic> toJson() => {
'promptTemplateId': promptTemplateId,
'name': name,
'description': description,
'categories': categories,
'tags': tags,
if (expectedRootNodes != null) 'expectedRootNodes': expectedRootNodes,
if (maxDepth != null) 'maxDepth': maxDepth,
if (difficultyLevel != null) 'difficultyLevel': difficultyLevel,
'enabled': enabled,
};
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is StrategyTemplateInfo &&
other.promptTemplateId == promptTemplateId &&
other.name == name &&
other.description == description;
}
@override
int get hashCode => promptTemplateId.hashCode ^ name.hashCode ^ description.hashCode;
@override
String toString() => 'StrategyTemplateInfo(promptTemplateId: $promptTemplateId, name: $name)';
}

View File

@@ -0,0 +1,118 @@
import 'package:equatable/equatable.dart';
import 'public_model_config.dart';
import 'user_ai_model_config_model.dart';
/// 统一的AI模型接口
/// 可以同时表示用户私有模型和公共模型
abstract class UnifiedAIModel extends Equatable {
/// 模型ID
String get id;
/// 提供商
String get provider;
/// 模型名称/标识
String get modelId;
/// 显示名称
String get displayName;
/// 是否为公共模型
bool get isPublic;
/// 是否已验证
bool get isValidated;
/// 积分倍率显示文本(仅公共模型有效)
String get creditMultiplierDisplay;
/// 获取模型标签(如 [系统]、[积分x1.2] 等)
List<String> get modelTags;
}
/// 用户私有模型包装器
class PrivateAIModel extends UnifiedAIModel {
final UserAIModelConfigModel _model;
PrivateAIModel(this._model);
@override
String get id => _model.id;
@override
String get provider => _model.provider;
@override
String get modelId => _model.modelName;
@override
String get displayName => _model.name;
@override
bool get isPublic => false;
@override
bool get isValidated => _model.isValidated;
@override
String get creditMultiplierDisplay => '';
@override
List<String> get modelTags => ['私有'];
/// 获取原始的用户模型配置
UserAIModelConfigModel get userConfig => _model;
@override
List<Object?> get props => [_model];
}
/// 公共模型包装器
class PublicAIModel extends UnifiedAIModel {
final PublicModel _model;
PublicAIModel(this._model);
@override
String get id => _model.id;
@override
String get provider => _model.provider;
@override
String get modelId => _model.modelId;
@override
String get displayName => _model.displayName;
@override
bool get isPublic => true;
@override
bool get isValidated => true; // 公共模型默认已验证
@override
String get creditMultiplierDisplay => _model.creditMultiplierDisplay;
@override
List<String> get modelTags {
final tags = <String>['系统'];
if (_model.creditMultiplierDisplay.isNotEmpty) {
tags.add(_model.creditMultiplierDisplay);
}
if (_model.recommended == true) {
tags.add('推荐');
}
if (_model.tags != null) {
tags.addAll(_model.tags!);
}
return tags;
}
/// 获取原始的公共模型配置
PublicModel get publicConfig => _model;
@override
List<Object?> get props => [_model];
}

View File

@@ -0,0 +1,169 @@
import 'package:meta/meta.dart'; // For @immutable
import '../utils/date_time_parser.dart'; // Import the parser
import 'package:equatable/equatable.dart'; // Import Equatable for Equatable mixin
/// 用户 AI 模型配置模型 (对应后端的 UserAIModelConfigResponse)
@immutable // Good practice for value objects
class UserAIModelConfigModel extends Equatable {
final String id;
final String userId;
final String provider;
final String modelName;
final String alias;
final String apiEndpoint;
final bool isValidated;
final bool isDefault;
final DateTime createdAt;
final DateTime updatedAt;
final String? apiKey; // 添加apiKey字段存储解密后的API密钥
/// 获取模型名称,用于显示
String get name => (alias.isNotEmpty && alias != modelName) ? alias : modelName;
const UserAIModelConfigModel({
required this.id,
required this.userId,
required this.provider,
required this.modelName,
required this.alias,
required this.apiEndpoint,
required this.isValidated,
required this.isDefault,
required this.createdAt,
required this.updatedAt,
this.apiKey, // 添加apiKey字段可为空
});
// 空实例,用于默认值
factory UserAIModelConfigModel.empty() {
return UserAIModelConfigModel(
id: '',
userId: '',
provider: '',
modelName: '',
alias: '',
apiEndpoint: '',
isValidated: false,
isDefault: false,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
apiKey: null, // 默认为null
);
}
// 从JSON转换方法
factory UserAIModelConfigModel.fromJson(Map<String, dynamic> json) {
// Helper to safely get string, providing a default if null or wrong type
String safeString(String key, [String defaultValue = '']) {
return json[key] is String ? json[key] as String : defaultValue;
}
// Helper to safely get bool, providing a default if null or wrong type
bool safeBool(String key, [bool defaultValue = false]) {
return json[key] is bool ? json[key] as bool : defaultValue;
}
return UserAIModelConfigModel(
id: safeString('id'), // Assuming 'id' is the key from backend
userId: safeString('userId'),
provider: safeString('provider'),
modelName: safeString('modelName'),
alias: safeString('alias'), // 使用safeString确保null安全
apiEndpoint: safeString('apiEndpoint'), // 修复使用safeString处理可能为null的apiEndpoint
isValidated: safeBool('isValidated'),
isDefault: safeBool('isDefault'),
createdAt: parseBackendDateTime(json['createdAt']), // Use the parser
updatedAt: parseBackendDateTime(json['updatedAt']), // Use the parser
apiKey: json['apiKey'] as String?, // 添加API密钥可为空
);
}
// 转换为JSON方法
Map<String, dynamic> toJson() {
return {
'id': id,
'userId': userId,
'provider': provider,
'modelName': modelName,
'alias': alias,
'apiEndpoint': apiEndpoint,
'isValidated': isValidated,
'isDefault': isDefault,
'createdAt': createdAt.toIso8601String(), // Standard format for JSON
'updatedAt': updatedAt.toIso8601String(), // Standard format for JSON
'apiKey': apiKey, // 包含API密钥
};
}
// 复制方法
UserAIModelConfigModel copyWith({
String? id,
String? userId,
String? provider,
String? modelName,
String? alias,
String? apiEndpoint,
bool? isValidated,
bool? isDefault,
DateTime? createdAt,
DateTime? updatedAt,
String? apiKey, // 添加apiKey参数
}) {
return UserAIModelConfigModel(
id: id ?? this.id,
userId: userId ?? this.userId,
provider: provider ?? this.provider,
modelName: modelName ?? this.modelName,
alias: alias ?? this.alias,
apiEndpoint: apiEndpoint ?? this.apiEndpoint,
isValidated: isValidated ?? this.isValidated,
isDefault: isDefault ?? this.isDefault,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
apiKey: apiKey ?? this.apiKey, // 复制apiKey
);
}
// --- Value Equality ---
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UserAIModelConfigModel &&
other.id == id &&
other.userId == userId &&
other.provider == provider &&
other.modelName == modelName &&
other.alias == alias &&
other.apiEndpoint == apiEndpoint &&
other.isValidated == isValidated &&
other.isDefault == isDefault &&
other.createdAt == createdAt &&
other.apiKey == apiKey && // 比较apiKey
other.updatedAt == updatedAt;
}
@override
int get hashCode {
return id.hashCode ^
userId.hashCode ^
provider.hashCode ^
modelName.hashCode ^
alias.hashCode ^
apiEndpoint.hashCode ^
isValidated.hashCode ^
isDefault.hashCode ^
createdAt.hashCode ^
apiKey.hashCode ^ // 计算apiKey的哈希值
updatedAt.hashCode;
}
@override
String toString() {
return 'UserAIModelConfigModel(id: $id, userId: $userId, provider: $provider, modelName: $modelName, alias: $alias, apiEndpoint: $apiEndpoint, isValidated: $isValidated, isDefault: $isDefault, createdAt: $createdAt, updatedAt: $updatedAt, apiKey: ${apiKey != null ? '******' : 'null'})'; // 不显示完整apiKey
}
@override
List<Object?> get props => [id, userId, provider, modelName, alias, apiEndpoint, isValidated, isDefault, createdAt, updatedAt, apiKey]; // 添加apiKey到props
}

View File

@@ -0,0 +1,64 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user_credit.g.dart';
/// 用户积分模型
@JsonSerializable()
class UserCredit extends Equatable {
/// 用户ID
final String userId;
/// 积分余额
final int credits;
/// 积分与美元汇率(可选)
final double? creditToUsdRate;
const UserCredit({
required this.userId,
required this.credits,
this.creditToUsdRate,
});
factory UserCredit.fromJson(Map<String, dynamic> json) =>
_$UserCreditFromJson(json);
Map<String, dynamic> toJson() => _$UserCreditToJson(this);
@override
List<Object?> get props => [userId, credits, creditToUsdRate];
/// 获取格式化的积分显示文本
String get formattedCredits {
if (credits >= 1000000) {
return '${(credits / 1000000).toStringAsFixed(1)}M';
} else if (credits >= 1000) {
return '${(credits / 1000).toStringAsFixed(1)}K';
} else {
return credits.toString();
}
}
/// 获取等值美元显示(如果有汇率信息)
String get equivalentUsd {
if (creditToUsdRate != null && creditToUsdRate! > 0) {
final usd = credits / creditToUsdRate!;
return '\$${usd.toStringAsFixed(2)}';
}
return '';
}
/// 检查是否有足够积分
bool hasEnoughCredits(int required) {
return credits >= required;
}
/// 创建空积分对象
factory UserCredit.empty() {
return const UserCredit(
userId: '',
credits: 0,
);
}
}

View File

@@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_credit.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UserCredit _$UserCreditFromJson(Map<String, dynamic> json) => $checkedCreate(
'UserCredit',
json,
($checkedConvert) {
final val = UserCredit(
userId: $checkedConvert('userId', (v) => v as String),
credits: $checkedConvert('credits', (v) => (v as num).toInt()),
creditToUsdRate: $checkedConvert(
'creditToUsdRate', (v) => (v as num?)?.toDouble()),
);
return val;
},
);
Map<String, dynamic> _$UserCreditToJson(UserCredit instance) {
final val = <String, dynamic>{
'userId': instance.userId,
'credits': instance.credits,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('creditToUsdRate', instance.creditToUsdRate);
return val;
}