feat: 角色卡 demo

This commit is contained in:
liqupan
2026-01-28 20:28:38 +08:00
parent a4e7898e94
commit c09fbf4808
35 changed files with 2773 additions and 329 deletions

View 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,
};
}
}

View 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,
);
}
}

View 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(),
);
}
}

View 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';
}

View 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';