AI人设可以配置

This commit is contained in:
2026-09-28 17:16:34 +08:00
parent 2dd89bfde0
commit 80222099ff
10 changed files with 1269 additions and 23 deletions
+77
View File
@@ -0,0 +1,77 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../api/ai_api.dart';
import '../core/storage/local_storage.dart';
import '../models/ai_persona.dart';
/// 可选的人设列表(含虚拟的「默认」项)
///
/// 与 [aiConfigsProvider] 完全平行:一个拉模型配置,一个拉人设。
final aiPersonasProvider = FutureProvider.autoDispose<List<AiPersonaItem>>((ref) {
return ref.watch(aiApiProvider).personas();
});
/// 当前选中的人设。
///
/// **不设 autoDispose**:理由和 [SelectedAiConfigNotifier] 一样 ——
/// 选完回对话页要立刻生效,切页面不该丢。
class SelectedAiPersonaNotifier extends Notifier<int?> {
@override
int? build() {
// 存的是「真实 id」,null = 用内置默认。
// 默认项在列表里的 id 也是 null,两边语义天然一致。
return LocalStorage.instance.getAiPersonaId();
}
void select(int? id) {
state = id;
LocalStorage.instance.saveAiPersonaId(id);
}
}
final selectedAiPersonaProvider =
NotifierProvider<SelectedAiPersonaNotifier, int?>(SelectedAiPersonaNotifier.new);
/// 从人设列表里挑出当前选中的那一条。
///
/// 三种情况都要兜住:
/// · 还没选过 → 用列表第一项(也就是「默认」)
/// · 选过但那条被删了 → 退回第一项,并顺手把本地选择改掉
/// · 列表还没加载出来 → 返回 null,界面显示「--」
///
/// 注意这里和服务端的降级是**两套独立的安全网**:服务端拿到过期 id 也会
/// 退回内置默认,所以就算这里没来得及纠正,对话也不会挂。
final activeAiPersonaProvider = Provider.autoDispose<AiPersonaItem?>((ref) {
final personas = ref.watch(aiPersonasProvider).value;
if (personas == null || personas.isEmpty) return null;
final selectedId = ref.watch(selectedAiPersonaProvider);
if (selectedId == null) return personas.first;
for (final item in personas) {
if (item.selectionKey == selectedId) return item;
}
// 选中的那条已经不存在了(被删了)。退回默认。
// 放在微任务里改 state —— build 过程中不能同步改另一个 provider。
Future.microtask(() {
ref.read(selectedAiPersonaProvider.notifier).select(null);
});
return personas.first;
});
/// 内置默认人设文本,供编辑页「恢复默认」按钮使用。
///
/// 从列表的默认项里取(服务端下发),拿不到时返回 null ——
/// 这时按钮会提示「还没加载出来」,而不是塞一份 App 里硬编码的文本进去
/// (那样两边迟早不一致)。
final defaultPersonaPromptProvider = Provider.autoDispose<String?>((ref) {
final personas = ref.watch(aiPersonasProvider).value;
if (personas == null) return null;
for (final p in personas) {
if (p.isDefault && (p.defaultPrompt?.isNotEmpty ?? false)) {
return p.defaultPrompt;
}
}
return null;
});
+9 -3
View File
@@ -4,6 +4,7 @@ import '../api/ai_api.dart';
import '../core/network/api_exception.dart';
import '../models/ai.dart';
import 'ai_config_provider.dart';
import 'ai_persona_provider.dart';
import 'bill_refresh.dart';
import 'note_provider.dart';
@@ -160,9 +161,14 @@ class AssistantNotifier extends Notifier<AssistantState> {
try {
// 带上用户选中的配置。null = 用服务端 .env 那套(虚拟的「默认」项)
final configId = ref.read(selectedAiConfigProvider);
await for (final event in ref
.read(aiApiProvider)
.chat(sessionId: state.sessionId, configId: configId, content: trimmed)) {
// 人设是独立的另一套。null = 用服务端内置的默认人设
final personaId = ref.read(selectedAiPersonaProvider);
await for (final event in ref.read(aiApiProvider).chat(
sessionId: state.sessionId,
configId: configId,
personaId: personaId,
content: trimmed,
)) {
if (!ref.mounted) return;
switch (event.type) {