马良AI写作初始化仓库
This commit is contained in:
85
AINoval/lib/models/admin/admin_auth_models.dart
Normal file
85
AINoval/lib/models/admin/admin_auth_models.dart
Normal 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,
|
||||
];
|
||||
}
|
||||
295
AINoval/lib/models/admin/admin_models.dart
Normal file
295
AINoval/lib/models/admin/admin_models.dart
Normal 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,
|
||||
];
|
||||
}
|
||||
282
AINoval/lib/models/admin/admin_models.g.dart
Normal file
282
AINoval/lib/models/admin/admin_models.g.dart
Normal 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;
|
||||
}
|
||||
54
AINoval/lib/models/admin/billing_models.dart
Normal file
54
AINoval/lib/models/admin/billing_models.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
702
AINoval/lib/models/admin/llm_observability_models.dart
Normal file
702
AINoval/lib/models/admin/llm_observability_models.dart
Normal 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];
|
||||
}
|
||||
951
AINoval/lib/models/admin/llm_observability_models.g.dart
Normal file
951
AINoval/lib/models/admin/llm_observability_models.g.dart
Normal 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;
|
||||
}
|
||||
429
AINoval/lib/models/admin/subscription_models.dart
Normal file
429
AINoval/lib/models/admin/subscription_models.dart
Normal 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,
|
||||
];
|
||||
}
|
||||
191
AINoval/lib/models/admin/subscription_models.g.dart
Normal file
191
AINoval/lib/models/admin/subscription_models.g.dart
Normal 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,
|
||||
};
|
||||
107
AINoval/lib/models/ai_context_tracking.dart
Normal file
107
AINoval/lib/models/ai_context_tracking.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
408
AINoval/lib/models/ai_feature_form_config.dart
Normal file
408
AINoval/lib/models/ai_feature_form_config.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
123
AINoval/lib/models/ai_model_group.dart
Normal file
123
AINoval/lib/models/ai_model_group.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
680
AINoval/lib/models/ai_request_models.dart
Normal file
680
AINoval/lib/models/ai_request_models.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
213
AINoval/lib/models/analytics_data.dart
Normal file
213
AINoval/lib/models/analytics_data.dart
Normal 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 '日期范围';
|
||||
}
|
||||
}
|
||||
}
|
||||
67
AINoval/lib/models/api/editor_dtos.dart
Normal file
67
AINoval/lib/models/api/editor_dtos.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
207
AINoval/lib/models/app_registration_config.dart
Normal file
207
AINoval/lib/models/app_registration_config.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
89
AINoval/lib/models/chapters_for_preload_dto.dart
Normal file
89
AINoval/lib/models/chapters_for_preload_dto.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
156
AINoval/lib/models/chat_message.dart
Normal file
156
AINoval/lib/models/chat_message.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
456
AINoval/lib/models/chat_models.dart
Normal file
456
AINoval/lib/models/chat_models.dart
Normal 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 }
|
||||
41
AINoval/lib/models/compose_preview.dart
Normal file
41
AINoval/lib/models/compose_preview.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
1193
AINoval/lib/models/context_selection_models.dart
Normal file
1193
AINoval/lib/models/context_selection_models.dart
Normal file
File diff suppressed because it is too large
Load Diff
252
AINoval/lib/models/dto/novel_setting_dto.dart
Normal file
252
AINoval/lib/models/dto/novel_setting_dto.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
170
AINoval/lib/models/editor_content.dart
Normal file
170
AINoval/lib/models/editor_content.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
371
AINoval/lib/models/editor_settings.dart
Normal file
371
AINoval/lib/models/editor_settings.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
57
AINoval/lib/models/import_status.dart
Normal file
57
AINoval/lib/models/import_status.dart
Normal 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}';
|
||||
}
|
||||
48
AINoval/lib/models/model_info.dart
Normal file
48
AINoval/lib/models/model_info.dart
Normal 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*/];
|
||||
}
|
||||
334
AINoval/lib/models/next_outline/next_outline_dto.dart
Normal file
334
AINoval/lib/models/next_outline/next_outline_dto.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
291
AINoval/lib/models/novel_setting_item.dart
Normal file
291
AINoval/lib/models/novel_setting_item.dart
Normal 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];
|
||||
}
|
||||
312
AINoval/lib/models/novel_snippet.dart
Normal file
312
AINoval/lib/models/novel_snippet.dart
Normal 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);
|
||||
}
|
||||
386
AINoval/lib/models/novel_snippet.g.dart
Normal file
386
AINoval/lib/models/novel_snippet.g.dart
Normal 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;
|
||||
}
|
||||
950
AINoval/lib/models/novel_structure.dart
Normal file
950
AINoval/lib/models/novel_structure.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
212
AINoval/lib/models/novel_summary.dart
Normal file
212
AINoval/lib/models/novel_summary.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
160
AINoval/lib/models/novel_with_summaries_dto.dart
Normal file
160
AINoval/lib/models/novel_with_summaries_dto.dart
Normal 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; // 出错时返回原始小说模型
|
||||
}
|
||||
}
|
||||
}
|
||||
1242
AINoval/lib/models/preset_models.dart
Normal file
1242
AINoval/lib/models/preset_models.dart
Normal file
File diff suppressed because it is too large
Load Diff
1833
AINoval/lib/models/prompt_models.dart
Normal file
1833
AINoval/lib/models/prompt_models.dart
Normal file
File diff suppressed because it is too large
Load Diff
719
AINoval/lib/models/public_model_config.dart
Normal file
719
AINoval/lib/models/public_model_config.dart
Normal 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);
|
||||
}
|
||||
598
AINoval/lib/models/public_model_config.g.dart
Normal file
598
AINoval/lib/models/public_model_config.g.dart
Normal 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;
|
||||
}
|
||||
48
AINoval/lib/models/revision.dart
Normal file
48
AINoval/lib/models/revision.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
56
AINoval/lib/models/save_result.dart
Normal file
56
AINoval/lib/models/save_result.dart
Normal 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)';
|
||||
}
|
||||
459
AINoval/lib/models/scene_beat_data.dart
Normal file
459
AINoval/lib/models/scene_beat_data.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
108
AINoval/lib/models/scene_summary_dto.dart
Normal file
108
AINoval/lib/models/scene_summary_dto.dart
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
112
AINoval/lib/models/scene_version.dart
Normal file
112
AINoval/lib/models/scene_version.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
397
AINoval/lib/models/setting_generation_event.dart
Normal file
397
AINoval/lib/models/setting_generation_event.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
346
AINoval/lib/models/setting_generation_session.dart
Normal file
346
AINoval/lib/models/setting_generation_session.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
82
AINoval/lib/models/setting_group.dart
Normal file
82
AINoval/lib/models/setting_group.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
117
AINoval/lib/models/setting_node.dart
Normal file
117
AINoval/lib/models/setting_node.dart
Normal 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,
|
||||
}
|
||||
99
AINoval/lib/models/setting_relationship_type.dart
Normal file
99
AINoval/lib/models/setting_relationship_type.dart
Normal 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],
|
||||
};
|
||||
}
|
||||
}
|
||||
70
AINoval/lib/models/setting_type.dart
Normal file
70
AINoval/lib/models/setting_type.dart
Normal 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});
|
||||
}
|
||||
240
AINoval/lib/models/strategy_response.dart
Normal file
240
AINoval/lib/models/strategy_response.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
86
AINoval/lib/models/strategy_template_info.dart
Normal file
86
AINoval/lib/models/strategy_template_info.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
/// 策略模板信息
|
||||
///
|
||||
/// 对应后端 ISettingGenerationService.StrategyTemplateInfo DTO
|
||||
///
|
||||
/// 用于替代旧的 StrategyInfo,与新的后端API完全对齐
|
||||
class StrategyTemplateInfo {
|
||||
/// 策略模板ID(promptTemplateId)
|
||||
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)';
|
||||
}
|
||||
118
AINoval/lib/models/unified_ai_model.dart
Normal file
118
AINoval/lib/models/unified_ai_model.dart
Normal 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];
|
||||
}
|
||||
169
AINoval/lib/models/user_ai_model_config_model.dart
Normal file
169
AINoval/lib/models/user_ai_model_config_model.dart
Normal 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
|
||||
}
|
||||
64
AINoval/lib/models/user_credit.dart
Normal file
64
AINoval/lib/models/user_credit.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
37
AINoval/lib/models/user_credit.g.dart
Normal file
37
AINoval/lib/models/user_credit.g.dart
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user