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,31 @@
import 'dart:io';
/// Supabase 配置
///
/// 这里配置了本地 Docker 部署的 Supabase 连接信息
/// 生产环境部署时需要替换为实际的 URL 和 Key
class SupabaseConfig {
SupabaseConfig._();
/// Supabase API URL
///
/// - Android 模拟器: 使用 10.0.2.2 访问主机
/// - iOS 模拟器/macOS: 使用 localhost
/// - 真机测试: 需要替换为电脑的局域网 IP 地址
static String get url {
// Android 模拟器需要使用 10.0.2.2 来访问主机
if (Platform.isAndroid) {
return 'http://10.0.2.2:8000';
}
// 其他平台使用 localhost
return 'http://localhost:8000';
}
/// Supabase Anon Key
/// 这个 key 是公开的,用于客户端访问
static const String anonKey =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY5NTk1NjI4LCJleHAiOjE5MjcyNzU2Mjh9.moV0JpCSx3Y1QTZmKZ5K-tQLaWcshxtxFlCoIBQFsEU';
/// 是否启用调试模式
static const bool debug = true;
}

View File

@@ -0,0 +1,17 @@
/// Core 模块导出
///
/// 统一导出所有 core 模块的公共 API
library core;
// 配置
export 'config/supabase_config.dart';
// 服务
export 'services/services.dart';
// 模型
export 'models/models.dart';
// 仓库
export 'repositories/repositories.dart';

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

View File

@@ -0,0 +1,164 @@
import 'package:flutter/foundation.dart';
import '../services/supabase_service.dart';
import '../models/character_model.dart';
import '../models/category_model.dart';
/// 角色仓库
///
/// 负责从 Supabase 获取角色相关数据
class CharacterRepository {
CharacterRepository._();
/// 获取所有分类
static Future<List<CategoryModel>> getCategories() async {
try {
final response = await SupabaseService.from('categories')
.select()
.eq('is_active', true)
.order('sort_order', ascending: true);
return (response as List)
.map((json) => CategoryModel.fromJson(json))
.toList();
} catch (e) {
debugPrint('❌ 获取分类失败: $e');
rethrow;
}
}
/// 获取所有角色(包含标签)
///
/// 使用 characters_with_tags 视图获取完整数据
static Future<List<CharacterModel>> getCharacters() async {
try {
final response = await SupabaseService.from('characters_with_tags')
.select();
return (response as List)
.map((json) => CharacterModel.fromJson(json))
.toList();
} catch (e) {
debugPrint('❌ 获取角色列表失败: $e');
rethrow;
}
}
/// 根据分类筛选角色
///
/// [categoryCode] 分类代码,如 'gentle', 'dom' 等
/// 'all' 返回所有角色
static Future<List<CharacterModel>> getCharactersByCategory(String categoryCode) async {
try {
// 如果是 'all',直接获取所有
if (categoryCode == 'all') {
return getCharacters();
}
// 获取该分类下的标签
final tagsResponse = await SupabaseService.from('tags')
.select('name')
.eq('category_id',
SupabaseService.from('categories')
.select('id')
.eq('code', categoryCode)
.single()
);
final tagNames = (tagsResponse as List)
.map((t) => t['name'] as String)
.toList();
if (tagNames.isEmpty) {
return [];
}
// 获取包含这些标签的角色
final allCharacters = await getCharacters();
return allCharacters.where((char) {
return char.tagNames.any((tag) => tagNames.contains(tag));
}).toList();
} catch (e) {
debugPrint('❌ 按分类获取角色失败: $e');
// 如果查询失败,返回本地筛选
return _filterCharactersLocally(await getCharacters(), categoryCode);
}
}
/// 本地筛选角色(作为后备方案)
static List<CharacterModel> _filterCharactersLocally(
List<CharacterModel> characters,
String categoryCode
) {
if (categoryCode == 'all') return characters;
return characters.where((c) {
final tags = c.tagNames.join('');
switch (categoryCode) {
case 'gentle':
return tags.contains('治愈') || tags.contains('温顺') || tags.contains('医疗');
case 'dom':
return tags.contains('强势') || tags.contains('调教') || tags.contains('指令');
case 'wild':
return tags.contains('病娇') || tags.contains('神秘') ||
tags.contains('不稳定') || tags.contains('极乐');
case 'exclusive':
return c.isLocked;
default:
return false;
}
}).toList();
}
/// 根据 ID 获取单个角色
static Future<CharacterModel?> getCharacterById(String id) async {
try {
final response = await SupabaseService.from('characters_with_tags')
.select()
.eq('id', id)
.maybeSingle();
if (response == null) return null;
return CharacterModel.fromJson(response);
} catch (e) {
debugPrint('❌ 获取角色详情失败: $e');
rethrow;
}
}
/// 获取角色头像的公开 URL
///
/// [avatarPath] Storage 中的路径
static String getAvatarUrl(String? avatarPath) {
if (avatarPath == null || avatarPath.isEmpty) {
// 返回默认头像
return 'https://via.placeholder.com/300x400?text=No+Avatar';
}
// 如果已经是完整 URL直接返回
if (avatarPath.startsWith('http')) {
return avatarPath;
}
// 从 Storage 获取公开 URL
return SupabaseService.storage
.from('avatars')
.getPublicUrl(avatarPath);
}
/// 获取仅会员可见的角色
static Future<List<CharacterModel>> getLockedCharacters() async {
try {
final response = await SupabaseService.from('characters_with_tags')
.select()
.eq('is_locked', true);
return (response as List)
.map((json) => CharacterModel.fromJson(json))
.toList();
} catch (e) {
debugPrint('❌ 获取会员角色失败: $e');
rethrow;
}
}
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/foundation.dart';
import '../services/supabase_service.dart';
import '../models/llm_config_model.dart';
/// LLM 配置仓库
///
/// 负责从 Supabase 获取 LLM 配置
class LlmConfigRepository {
LlmConfigRepository._();
static LlmConfigModel? _cachedConfig;
/// 获取当前激活的 LLM 配置
///
/// 开发阶段默认强制刷新,生产环境可改为 false 使用缓存
static Future<LlmConfigModel?> getActiveConfig({bool forceRefresh = true}) async {
if (_cachedConfig != null && !forceRefresh) {
return _cachedConfig;
}
try {
final response = await SupabaseService.from('llm_config')
.select()
.eq('is_active', true)
.maybeSingle();
if (response == null) {
debugPrint('⚠️ 没有找到激活的 LLM 配置');
return null;
}
_cachedConfig = LlmConfigModel.fromJson(response);
debugPrint('✅ LLM 配置已加载: ${_cachedConfig!.model}');
return _cachedConfig;
} catch (e) {
debugPrint('❌ 获取 LLM 配置失败: $e');
rethrow;
}
}
/// 清除缓存
static void clearCache() {
_cachedConfig = null;
}
/// 更新配置(需要管理员权限)
static Future<void> updateConfig(LlmConfigModel config) async {
try {
await SupabaseService.from('llm_config')
.update(config.toJson())
.eq('id', config.id);
_cachedConfig = config;
debugPrint('✅ LLM 配置已更新');
} catch (e) {
debugPrint('❌ 更新 LLM 配置失败: $e');
rethrow;
}
}
}

View File

@@ -0,0 +1,5 @@
/// Core Repositories 导出
library repositories;
export 'character_repository.dart';
export 'llm_config_repository.dart';

View File

@@ -0,0 +1,183 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../models/chat_message_model.dart';
import '../models/llm_config_model.dart';
import '../models/character_model.dart';
import '../repositories/llm_config_repository.dart';
/// AI 聊天服务
///
/// 负责调用 LLM API 进行对话
class ChatService {
ChatService._();
/// 发送消息并获取 AI 回复
///
/// [character] 当前对话的角色
/// [messages] 历史消息列表
/// [userMessage] 用户新发送的消息
/// [onStream] 流式输出回调(每次收到新内容时调用)
static Future<String> sendMessage({
required CharacterModel character,
required List<ChatMessage> messages,
required String userMessage,
Function(String)? onStream,
}) async {
// 获取 LLM 配置
final config = await LlmConfigRepository.getActiveConfig();
if (config == null) {
throw Exception('LLM 配置未找到,请先配置 API Key');
}
// 构建消息列表
final apiMessages = <Map<String, String>>[];
// 1. System prompt角色人设
final systemPrompt = character.aiSystemPrompt ??
'你是 ${character.name}${character.tagline ?? "一个AI角色"}${character.description ?? ""}';
apiMessages.add({'role': 'system', 'content': systemPrompt});
// 2. 历史消息(最多保留最近 20 条)
final recentMessages = messages.length > 20
? messages.sublist(messages.length - 20)
: messages;
for (final msg in recentMessages) {
if (msg.role != 'system') {
apiMessages.add(msg.toApiFormat());
}
}
// 3. 新用户消息
apiMessages.add({'role': 'user', 'content': userMessage});
debugPrint('📤 发送到 LLM: ${apiMessages.length} 条消息');
// 判断是否使用流式输出
if (config.stream && onStream != null) {
return _sendStreamRequest(config, apiMessages, onStream);
} else {
return _sendNormalRequest(config, apiMessages);
}
}
/// 普通请求(非流式)
static Future<String> _sendNormalRequest(
LlmConfigModel config,
List<Map<String, String>> messages,
) async {
try {
final response = await http.post(
Uri.parse(config.chatCompletionsUrl),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${config.apiKey}',
},
body: jsonEncode({
'model': config.model,
'messages': messages,
'temperature': config.temperature,
'max_tokens': config.maxTokens,
'stream': false,
}),
);
if (response.statusCode != 200) {
debugPrint('❌ LLM API 错误: ${response.statusCode} ${response.body}');
throw Exception('API 请求失败: ${response.statusCode}');
}
final data = jsonDecode(response.body);
final content = data['choices'][0]['message']['content'] as String;
debugPrint('📥 收到回复: ${content.substring(0, content.length.clamp(0, 50))}...');
return content;
} catch (e) {
debugPrint('❌ LLM 请求失败: $e');
rethrow;
}
}
/// 流式请求
static Future<String> _sendStreamRequest(
LlmConfigModel config,
List<Map<String, String>> messages,
Function(String) onStream,
) async {
try {
final client = http.Client();
final request = http.Request('POST', Uri.parse(config.chatCompletionsUrl));
request.headers.addAll({
'Content-Type': 'application/json',
'Authorization': 'Bearer ${config.apiKey}',
'Accept': 'text/event-stream',
});
request.body = jsonEncode({
'model': config.model,
'messages': messages,
'temperature': config.temperature,
'max_tokens': config.maxTokens,
'stream': true,
});
final response = await client.send(request);
if (response.statusCode != 200) {
final body = await response.stream.bytesToString();
debugPrint('❌ LLM API 错误: ${response.statusCode} $body');
throw Exception('API 请求失败: ${response.statusCode}');
}
final completer = Completer<String>();
final buffer = StringBuffer();
response.stream
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(
(line) {
if (line.startsWith('data: ')) {
final data = line.substring(6);
if (data == '[DONE]') {
return;
}
try {
final json = jsonDecode(data);
final delta = json['choices']?[0]?['delta']?['content'];
if (delta != null && delta is String) {
buffer.write(delta);
onStream(buffer.toString());
}
} catch (e) {
// 忽略解析错误
}
}
},
onDone: () {
client.close();
final result = buffer.toString();
debugPrint('📥 流式回复完成: ${result.length} 字符');
completer.complete(result);
},
onError: (e) {
client.close();
debugPrint('❌ 流式请求错误: $e');
completer.completeError(e);
},
);
return completer.future;
} catch (e) {
debugPrint('❌ LLM 流式请求失败: $e');
rethrow;
}
}
/// 获取角色的问候语
static String getGreeting(CharacterModel character) {
return character.aiGreeting ??
'你好,我是 ${character.name}。有什么我可以帮你的吗?';
}
}

View File

@@ -0,0 +1,78 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/chat_message_model.dart';
/// 本地聊天存储服务
///
/// 使用 SharedPreferences 存储聊天记录
class ChatStorageService {
ChatStorageService._();
static const String _keyPrefix = 'chat_session_';
/// 获取聊天会话
static Future<ChatSession> getSession(String characterId) async {
try {
final prefs = await SharedPreferences.getInstance();
final key = '$_keyPrefix$characterId';
final data = prefs.getString(key);
if (data == null) {
return ChatSession.empty(characterId);
}
return ChatSession.fromJson(jsonDecode(data));
} catch (e) {
debugPrint('❌ 读取聊天记录失败: $e');
return ChatSession.empty(characterId);
}
}
/// 保存聊天会话
static Future<void> saveSession(ChatSession session) async {
try {
final prefs = await SharedPreferences.getInstance();
final key = '$_keyPrefix${session.characterId}';
await prefs.setString(key, jsonEncode(session.toJson()));
debugPrint('✅ 聊天记录已保存 (${session.messages.length} 条消息)');
} catch (e) {
debugPrint('❌ 保存聊天记录失败: $e');
}
}
/// 添加消息到会话
static Future<ChatSession> addMessage(String characterId, ChatMessage message) async {
final session = await getSession(characterId);
final newSession = session.addMessage(message);
await saveSession(newSession);
return newSession;
}
/// 清除角色的聊天记录
static Future<void> clearSession(String characterId) async {
try {
final prefs = await SharedPreferences.getInstance();
final key = '$_keyPrefix$characterId';
await prefs.remove(key);
debugPrint('✅ 聊天记录已清除');
} catch (e) {
debugPrint('❌ 清除聊天记录失败: $e');
}
}
/// 获取所有聊天会话的角色 ID 列表
static Future<List<String>> getAllSessionIds() async {
try {
final prefs = await SharedPreferences.getInstance();
final keys = prefs.getKeys();
return keys
.where((k) => k.startsWith(_keyPrefix))
.map((k) => k.substring(_keyPrefix.length))
.toList();
} catch (e) {
debugPrint('❌ 获取会话列表失败: $e');
return [];
}
}
}

View File

@@ -0,0 +1,6 @@
/// Core Services 导出
library services;
export 'supabase_service.dart';
export 'chat_service.dart';
export 'chat_storage_service.dart';

View File

@@ -0,0 +1,60 @@
import 'package:flutter/foundation.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../config/supabase_config.dart';
/// Supabase 服务
///
/// 负责初始化和管理 Supabase 客户端
class SupabaseService {
SupabaseService._();
static bool _initialized = false;
/// 初始化 Supabase
///
/// 应该在 main() 函数中调用,确保在 runApp 之前完成
static Future<void> initialize() async {
if (_initialized) {
debugPrint('⚠️ Supabase 已经初始化过了');
return;
}
try {
await Supabase.initialize(
url: SupabaseConfig.url,
anonKey: SupabaseConfig.anonKey,
debug: SupabaseConfig.debug,
);
_initialized = true;
debugPrint('✅ Supabase 初始化成功');
debugPrint(' URL: ${SupabaseConfig.url}');
} catch (e) {
debugPrint('❌ Supabase 初始化失败: $e');
rethrow;
}
}
/// 获取 Supabase 客户端实例
static SupabaseClient get client => Supabase.instance.client;
/// 获取当前登录用户
static User? get currentUser => client.auth.currentUser;
/// 检查用户是否已登录
static bool get isLoggedIn => currentUser != null;
/// 获取 Auth 实例
static GoTrueClient get auth => client.auth;
/// 获取数据库实例
static SupabaseQueryBuilder from(String table) => client.from(table);
/// 获取存储实例
static SupabaseStorageClient get storage => client.storage;
/// 获取 Realtime 实例
static RealtimeClient get realtime => client.realtime;
/// 获取 Functions 实例
static FunctionsClient get functions => client.functions;
}