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