AI配置化

This commit is contained in:
2026-09-28 11:57:54 +08:00
parent 6176c49964
commit d52775ec10
8 changed files with 839 additions and 4 deletions
+71 -2
View File
@@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/network/api_exception.dart';
import '../core/network/dio_client.dart';
import '../models/ai.dart';
import '../models/ai_config.dart';
import '../models/json_util.dart';
final aiApiProvider = Provider<AiApi>((ref) => AiApi(ref.watch(dioProvider)));
@@ -67,12 +68,13 @@ class AiApi {
/// (`event: xxx` / `data: {...}` / 空行分隔),手动解析比多引一个依赖可控。
///
/// 注意:[sessionId] 传 null 表示开新会话,会话 ID 会由后端在 done 事件里返回。
Stream<AiEvent> chat({int? sessionId, required String content}) async* {
Stream<AiEvent> chat({int? sessionId, int? configId, required String content}) async* {
final Response<ResponseBody> res;
try {
res = await _dio.post<ResponseBody>(
'/ai/chat',
data: {'sessionId': sessionId, 'content': content},
// configId 传 null = 用服务端 .env 里那套(列表里虚拟的「默认」项)
data: {'sessionId': sessionId, 'configId': configId, 'content': content},
options: Options(
responseType: ResponseType.stream,
// LLM 生成慢 + 工具循环可能跑好几轮,给足;后端 SseEmitter 是 180s
@@ -131,6 +133,73 @@ class AiApi {
}
}
// ---------------------------------------------------------------- AI 配置
/// 配置列表。后端把虚拟的「默认」项放在第一位(id 为 null)。
Future<List<AiConfigItem>> configs() async {
try {
final res = await _dio.get('/ai/configs');
final data = res.data;
if (data is! List) return const [];
return asMapList(data).map(AiConfigItem.fromJson).toList();
} on DioException catch (e) {
final err = e.error;
throw err is ApiException ? err : ApiException(-1, '配置列表加载失败');
}
}
/// 新增。创建时 apiKey 必填。
Future<AiConfigItem> createConfig({
required String name,
required String baseUrl,
required String apiKey,
required String model,
}) async {
try {
final res = await _dio.post('/ai/configs', data: {
'name': name,
'baseUrl': baseUrl,
'apiKey': apiKey,
'model': model,
});
return AiConfigItem.fromJson(Map<String, dynamic>.from(res.data as Map));
} on DioException catch (e) {
final err = e.error;
throw err is ApiException ? err : ApiException(-1, '保存失败');
}
}
/// 更新。[apiKey] 传空表示不改动 —— App 只有掩码,不该被迫重新输入 key。
Future<AiConfigItem> updateConfig({
required int id,
required String name,
required String baseUrl,
required String model,
String? apiKey,
}) async {
try {
final res = await _dio.put('/ai/configs/$id', data: {
'name': name,
'baseUrl': baseUrl,
'model': model,
if (apiKey != null && apiKey.isNotEmpty) 'apiKey': apiKey,
});
return AiConfigItem.fromJson(Map<String, dynamic>.from(res.data as Map));
} on DioException catch (e) {
final err = e.error;
throw err is ApiException ? err : ApiException(-1, '保存失败');
}
}
Future<void> deleteConfig(int id) async {
try {
await _dio.delete('/ai/configs/$id');
} on DioException catch (e) {
final err = e.error;
throw err is ApiException ? err : ApiException(-1, '删除失败');
}
}
AiEvent? _parseEvent(String event, String payload) {
final decoded = jsonDecode(payload);
if (decoded is! Map<String, dynamic>) return null;