feat: 角色卡 demo
This commit is contained in:
43
wei_ai_app/lib/core/models/category_model.dart
Normal file
43
wei_ai_app/lib/core/models/category_model.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
/// 分类/筛选模型
|
||||
///
|
||||
/// 对应 Supabase 中的 categories 表
|
||||
class CategoryModel {
|
||||
final String id;
|
||||
final String code;
|
||||
final String label;
|
||||
final int sortOrder;
|
||||
final bool isActive;
|
||||
final DateTime? createdAt;
|
||||
|
||||
const CategoryModel({
|
||||
required this.id,
|
||||
required this.code,
|
||||
required this.label,
|
||||
this.sortOrder = 0,
|
||||
this.isActive = true,
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
factory CategoryModel.fromJson(Map<String, dynamic> json) {
|
||||
return CategoryModel(
|
||||
id: json['id'] as String,
|
||||
code: json['code'] as String,
|
||||
label: json['label'] as String,
|
||||
sortOrder: json['sort_order'] as int? ?? 0,
|
||||
isActive: json['is_active'] as bool? ?? true,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'code': code,
|
||||
'label': label,
|
||||
'sort_order': sortOrder,
|
||||
'is_active': isActive,
|
||||
};
|
||||
}
|
||||
}
|
||||
281
wei_ai_app/lib/core/models/character_model.dart
Normal file
281
wei_ai_app/lib/core/models/character_model.dart
Normal file
@@ -0,0 +1,281 @@
|
||||
/// AI 性格配置
|
||||
class AiPersonality {
|
||||
final double temperature;
|
||||
final List<String> traits;
|
||||
final String responseStyle;
|
||||
final List<String>? forbiddenTopics;
|
||||
final String? model;
|
||||
final int? maxTokens;
|
||||
|
||||
const AiPersonality({
|
||||
this.temperature = 0.7,
|
||||
this.traits = const [],
|
||||
this.responseStyle = '',
|
||||
this.forbiddenTopics,
|
||||
this.model,
|
||||
this.maxTokens,
|
||||
});
|
||||
|
||||
factory AiPersonality.fromJson(Map<String, dynamic> json) {
|
||||
return AiPersonality(
|
||||
temperature: (json['temperature'] as num?)?.toDouble() ?? 0.7,
|
||||
traits: (json['traits'] as List<dynamic>?)?.cast<String>() ?? [],
|
||||
responseStyle: json['response_style'] as String? ?? '',
|
||||
forbiddenTopics: (json['forbidden_topics'] as List<dynamic>?)?.cast<String>(),
|
||||
model: json['model'] as String?,
|
||||
maxTokens: json['max_tokens'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'temperature': temperature,
|
||||
'traits': traits,
|
||||
'response_style': responseStyle,
|
||||
if (forbiddenTopics != null) 'forbidden_topics': forbiddenTopics,
|
||||
if (model != null) 'model': model,
|
||||
if (maxTokens != null) 'max_tokens': maxTokens,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// AI 语音配置
|
||||
class AiVoiceConfig {
|
||||
final String? voiceId;
|
||||
final double speed;
|
||||
final double pitch;
|
||||
final String? emotion;
|
||||
|
||||
const AiVoiceConfig({
|
||||
this.voiceId,
|
||||
this.speed = 1.0,
|
||||
this.pitch = 1.0,
|
||||
this.emotion,
|
||||
});
|
||||
|
||||
factory AiVoiceConfig.fromJson(Map<String, dynamic> json) {
|
||||
return AiVoiceConfig(
|
||||
voiceId: json['voice_id'] as String?,
|
||||
speed: (json['speed'] as num?)?.toDouble() ?? 1.0,
|
||||
pitch: (json['pitch'] as num?)?.toDouble() ?? 1.0,
|
||||
emotion: json['emotion'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
if (voiceId != null) 'voice_id': voiceId,
|
||||
'speed': speed,
|
||||
'pitch': pitch,
|
||||
if (emotion != null) 'emotion': emotion,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 角色标签
|
||||
class CharacterTag {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? color;
|
||||
|
||||
const CharacterTag({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.color,
|
||||
});
|
||||
|
||||
factory CharacterTag.fromJson(Map<String, dynamic> json) {
|
||||
return CharacterTag(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
color: json['color'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
if (color != null) 'color': color,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 角色状态枚举
|
||||
enum CharacterStatus {
|
||||
online,
|
||||
busy,
|
||||
offline;
|
||||
|
||||
static CharacterStatus fromString(String? value) {
|
||||
switch (value) {
|
||||
case 'online':
|
||||
return CharacterStatus.online;
|
||||
case 'busy':
|
||||
return CharacterStatus.busy;
|
||||
case 'offline':
|
||||
return CharacterStatus.offline;
|
||||
default:
|
||||
return CharacterStatus.offline;
|
||||
}
|
||||
}
|
||||
|
||||
String get value {
|
||||
switch (this) {
|
||||
case CharacterStatus.online:
|
||||
return 'online';
|
||||
case CharacterStatus.busy:
|
||||
return 'busy';
|
||||
case CharacterStatus.offline:
|
||||
return 'offline';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 角色模型
|
||||
///
|
||||
/// 对应 Supabase 中的 characters 表
|
||||
class CharacterModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? tagline;
|
||||
final String? avatarPath;
|
||||
final String? description;
|
||||
final CharacterStatus status;
|
||||
final bool isLocked;
|
||||
final bool isActive;
|
||||
final int sortOrder;
|
||||
|
||||
// AI 配置
|
||||
final String? aiSystemPrompt;
|
||||
final String? aiGreeting;
|
||||
final AiPersonality aiPersonality;
|
||||
final AiVoiceConfig aiVoiceConfig;
|
||||
|
||||
// 关联的标签
|
||||
final List<CharacterTag> tags;
|
||||
|
||||
// 时间戳
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
const CharacterModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.tagline,
|
||||
this.avatarPath,
|
||||
this.description,
|
||||
this.status = CharacterStatus.offline,
|
||||
this.isLocked = false,
|
||||
this.isActive = true,
|
||||
this.sortOrder = 0,
|
||||
this.aiSystemPrompt,
|
||||
this.aiGreeting,
|
||||
this.aiPersonality = const AiPersonality(),
|
||||
this.aiVoiceConfig = const AiVoiceConfig(),
|
||||
this.tags = const [],
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
/// 从 JSON 创建(用于 Supabase 响应解析)
|
||||
factory CharacterModel.fromJson(Map<String, dynamic> json) {
|
||||
// 解析标签 - 可能来自视图的 jsonb 数组或单独查询
|
||||
List<CharacterTag> parseTags(dynamic tagsData) {
|
||||
if (tagsData == null) return [];
|
||||
if (tagsData is List) {
|
||||
return tagsData
|
||||
.where((t) => t != null && t is Map<String, dynamic>)
|
||||
.map((t) => CharacterTag.fromJson(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
return CharacterModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
tagline: json['tagline'] as String?,
|
||||
avatarPath: json['avatar_path'] as String?,
|
||||
description: json['description'] as String?,
|
||||
status: CharacterStatus.fromString(json['status'] as String?),
|
||||
isLocked: json['is_locked'] as bool? ?? false,
|
||||
isActive: json['is_active'] as bool? ?? true,
|
||||
sortOrder: json['sort_order'] as int? ?? 0,
|
||||
aiSystemPrompt: json['ai_system_prompt'] as String?,
|
||||
aiGreeting: json['ai_greeting'] as String?,
|
||||
aiPersonality: json['ai_personality'] != null
|
||||
? AiPersonality.fromJson(json['ai_personality'] as Map<String, dynamic>)
|
||||
: const AiPersonality(),
|
||||
aiVoiceConfig: json['ai_voice_config'] != null
|
||||
? AiVoiceConfig.fromJson(json['ai_voice_config'] as Map<String, dynamic>)
|
||||
: const AiVoiceConfig(),
|
||||
tags: parseTags(json['tags']),
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'] as String)
|
||||
: null,
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 转换为 JSON(用于插入/更新)
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'tagline': tagline,
|
||||
'avatar_path': avatarPath,
|
||||
'description': description,
|
||||
'status': status.value,
|
||||
'is_locked': isLocked,
|
||||
'is_active': isActive,
|
||||
'sort_order': sortOrder,
|
||||
'ai_system_prompt': aiSystemPrompt,
|
||||
'ai_greeting': aiGreeting,
|
||||
'ai_personality': aiPersonality.toJson(),
|
||||
'ai_voice_config': aiVoiceConfig.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 获取标签名称列表(用于兼容旧代码)
|
||||
List<String> get tagNames => tags.map((t) => t.name).toList();
|
||||
|
||||
/// 复制并修改部分字段
|
||||
CharacterModel copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? tagline,
|
||||
String? avatarPath,
|
||||
String? description,
|
||||
CharacterStatus? status,
|
||||
bool? isLocked,
|
||||
bool? isActive,
|
||||
int? sortOrder,
|
||||
String? aiSystemPrompt,
|
||||
String? aiGreeting,
|
||||
AiPersonality? aiPersonality,
|
||||
AiVoiceConfig? aiVoiceConfig,
|
||||
List<CharacterTag>? tags,
|
||||
}) {
|
||||
return CharacterModel(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
tagline: tagline ?? this.tagline,
|
||||
avatarPath: avatarPath ?? this.avatarPath,
|
||||
description: description ?? this.description,
|
||||
status: status ?? this.status,
|
||||
isLocked: isLocked ?? this.isLocked,
|
||||
isActive: isActive ?? this.isActive,
|
||||
sortOrder: sortOrder ?? this.sortOrder,
|
||||
aiSystemPrompt: aiSystemPrompt ?? this.aiSystemPrompt,
|
||||
aiGreeting: aiGreeting ?? this.aiGreeting,
|
||||
aiPersonality: aiPersonality ?? this.aiPersonality,
|
||||
aiVoiceConfig: aiVoiceConfig ?? this.aiVoiceConfig,
|
||||
tags: tags ?? this.tags,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
127
wei_ai_app/lib/core/models/chat_message_model.dart
Normal file
127
wei_ai_app/lib/core/models/chat_message_model.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
/// 聊天消息模型
|
||||
|
||||
/// 聊天消息模型
|
||||
class ChatMessage {
|
||||
final String id;
|
||||
final String role; // 'user', 'assistant', 'system'
|
||||
final String content;
|
||||
final DateTime timestamp;
|
||||
|
||||
const ChatMessage({
|
||||
required this.id,
|
||||
required this.role,
|
||||
required this.content,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
factory ChatMessage.fromJson(Map<String, dynamic> json) {
|
||||
return ChatMessage(
|
||||
id: json['id'] as String,
|
||||
role: json['role'] as String,
|
||||
content: json['content'] as String,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'role': role,
|
||||
'content': content,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 转换为 OpenAI API 格式
|
||||
Map<String, String> toApiFormat() {
|
||||
return {
|
||||
'role': role,
|
||||
'content': content,
|
||||
};
|
||||
}
|
||||
|
||||
/// 是否是用户消息
|
||||
bool get isUser => role == 'user';
|
||||
|
||||
/// 是否是 AI 消息
|
||||
bool get isAssistant => role == 'assistant';
|
||||
|
||||
/// 创建用户消息
|
||||
factory ChatMessage.user(String content) {
|
||||
return ChatMessage(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
role: 'user',
|
||||
content: content,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建 AI 消息
|
||||
factory ChatMessage.assistant(String content) {
|
||||
return ChatMessage(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
role: 'assistant',
|
||||
content: content,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建系统消息
|
||||
factory ChatMessage.system(String content) {
|
||||
return ChatMessage(
|
||||
id: 'system',
|
||||
role: 'system',
|
||||
content: content,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 聊天会话模型
|
||||
class ChatSession {
|
||||
final String characterId;
|
||||
final List<ChatMessage> messages;
|
||||
final DateTime lastUpdated;
|
||||
|
||||
const ChatSession({
|
||||
required this.characterId,
|
||||
required this.messages,
|
||||
required this.lastUpdated,
|
||||
});
|
||||
|
||||
factory ChatSession.fromJson(Map<String, dynamic> json) {
|
||||
return ChatSession(
|
||||
characterId: json['character_id'] as String,
|
||||
messages: (json['messages'] as List)
|
||||
.map((m) => ChatMessage.fromJson(m))
|
||||
.toList(),
|
||||
lastUpdated: DateTime.parse(json['last_updated'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'character_id': characterId,
|
||||
'messages': messages.map((m) => m.toJson()).toList(),
|
||||
'last_updated': lastUpdated.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 创建空会话
|
||||
factory ChatSession.empty(String characterId) {
|
||||
return ChatSession(
|
||||
characterId: characterId,
|
||||
messages: [],
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 添加消息并返回新会话
|
||||
ChatSession addMessage(ChatMessage message) {
|
||||
return ChatSession(
|
||||
characterId: characterId,
|
||||
messages: [...messages, message],
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
73
wei_ai_app/lib/core/models/llm_config_model.dart
Normal file
73
wei_ai_app/lib/core/models/llm_config_model.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
/// LLM 配置模型
|
||||
///
|
||||
/// 对应 Supabase 中的 llm_config 表
|
||||
class LlmConfigModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final bool isActive;
|
||||
|
||||
// API 连接
|
||||
final String apiBaseUrl;
|
||||
final String apiKey;
|
||||
final String model;
|
||||
|
||||
// 模型参数
|
||||
final double temperature;
|
||||
final int maxTokens;
|
||||
final bool stream;
|
||||
|
||||
// 时间戳
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
const LlmConfigModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.isActive = true,
|
||||
required this.apiBaseUrl,
|
||||
required this.apiKey,
|
||||
required this.model,
|
||||
this.temperature = 0.7,
|
||||
this.maxTokens = 2048,
|
||||
this.stream = true,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
factory LlmConfigModel.fromJson(Map<String, dynamic> json) {
|
||||
return LlmConfigModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
isActive: json['is_active'] as bool? ?? true,
|
||||
apiBaseUrl: json['api_base_url'] as String,
|
||||
apiKey: json['api_key'] as String,
|
||||
model: json['model'] as String,
|
||||
temperature: (json['temperature'] as num?)?.toDouble() ?? 0.7,
|
||||
maxTokens: json['max_tokens'] as int? ?? 2048,
|
||||
stream: json['stream'] as bool? ?? true,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'] as String)
|
||||
: null,
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'is_active': isActive,
|
||||
'api_base_url': apiBaseUrl,
|
||||
'api_key': apiKey,
|
||||
'model': model,
|
||||
'temperature': temperature,
|
||||
'max_tokens': maxTokens,
|
||||
'stream': stream,
|
||||
};
|
||||
}
|
||||
|
||||
/// 获取完整的 API URL(用于聊天完成)
|
||||
String get chatCompletionsUrl => '$apiBaseUrl/chat/completions';
|
||||
}
|
||||
7
wei_ai_app/lib/core/models/models.dart
Normal file
7
wei_ai_app/lib/core/models/models.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
/// Core Models 导出
|
||||
library models;
|
||||
|
||||
export 'character_model.dart';
|
||||
export 'category_model.dart';
|
||||
export 'llm_config_model.dart';
|
||||
export 'chat_message_model.dart';
|
||||
Reference in New Issue
Block a user