AI助手优化-头像和对话功能
This commit is contained in:
+41
-1
@@ -227,15 +227,17 @@ class AiApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// 新增一套人设。
|
||||
/// 新增一套人设。[avatar] 传 null 表示不设头像。
|
||||
Future<AiPersonaItem> createPersona({
|
||||
required String name,
|
||||
required String systemPrompt,
|
||||
String? avatar,
|
||||
}) async {
|
||||
try {
|
||||
final res = await _dio.post('/ai/personas', data: {
|
||||
'name': name,
|
||||
'systemPrompt': systemPrompt,
|
||||
'avatar': ?avatar,
|
||||
});
|
||||
return AiPersonaItem.fromJson(Map<String, dynamic>.from(res.data as Map));
|
||||
} on DioException catch (e) {
|
||||
@@ -246,15 +248,22 @@ class AiApi {
|
||||
|
||||
/// 修改人设。提示词是**整体替换**的 —— 每次都把完整文本传上来,
|
||||
/// 没有模型配置那种「key 留空 = 不改动」的语义。
|
||||
///
|
||||
/// 头像不一样:[avatar] 传 null = **不改动**(用户只改提示词时不该被迫重选头像),
|
||||
/// [clearAvatar] 传 true = 清空回到默认头像。
|
||||
Future<AiPersonaItem> updatePersona({
|
||||
required int id,
|
||||
required String name,
|
||||
required String systemPrompt,
|
||||
String? avatar,
|
||||
bool clearAvatar = false,
|
||||
}) async {
|
||||
try {
|
||||
final res = await _dio.put('/ai/personas/$id', data: {
|
||||
'name': name,
|
||||
'systemPrompt': systemPrompt,
|
||||
'avatar': ?avatar,
|
||||
if (clearAvatar) 'clearAvatar': true,
|
||||
});
|
||||
return AiPersonaItem.fromJson(Map<String, dynamic>.from(res.data as Map));
|
||||
} on DioException catch (e) {
|
||||
@@ -263,6 +272,30 @@ class AiApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传人设头像,返回**相对路径**(再由 create/update 提交)。
|
||||
///
|
||||
/// 只落盘不落库 —— 用户在编辑弹层里可能传了又取消,不该留下记录。
|
||||
Future<String> uploadPersonaAvatar(String filePath) async {
|
||||
try {
|
||||
final form = FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(filePath, filename: _fileName(filePath)),
|
||||
});
|
||||
final res = await _dio.post<dynamic>(
|
||||
'/ai/personas/avatar',
|
||||
data: form,
|
||||
options: Options(receiveTimeout: const Duration(seconds: 60)),
|
||||
);
|
||||
final data = res.data;
|
||||
if (data is Map && data['path'] != null) {
|
||||
return data['path'].toString();
|
||||
}
|
||||
throw ApiException(-1, '上传失败:服务端没返回路径');
|
||||
} on DioException catch (e) {
|
||||
final err = e.error;
|
||||
throw err is ApiException ? err : ApiException(-1, '头像上传失败');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deletePersona(int id) async {
|
||||
try {
|
||||
await _dio.delete('/ai/personas/$id');
|
||||
@@ -272,6 +305,13 @@ class AiApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// 从路径里取个带扩展名的文件名,后端靠它判断文件类型
|
||||
static String _fileName(String path) {
|
||||
final normalized = path.replaceAll('\\', '/');
|
||||
final name = normalized.substring(normalized.lastIndexOf('/') + 1);
|
||||
return name.isEmpty ? 'avatar.jpg' : name;
|
||||
}
|
||||
|
||||
AiEvent? _parseEvent(String event, String payload) {
|
||||
final decoded = jsonDecode(payload);
|
||||
if (decoded is! Map<String, dynamic>) return null;
|
||||
|
||||
@@ -39,6 +39,61 @@ class AuthApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// 拉当前用户资料。冷启动时本地缓存的 userInfo 可能过期(在别处换过头像),
|
||||
/// 用它刷一次。
|
||||
Future<AuthResponse> profile() async {
|
||||
try {
|
||||
final res = await _dio.get('/auth/profile');
|
||||
return AuthResponse.fromJson(_map(res.data));
|
||||
} on DioException catch (e) {
|
||||
throw _unwrap(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新资料。目前只有头像。
|
||||
///
|
||||
/// [avatar] 传 null = 不改动;[clearAvatar] 传 true = 清空。
|
||||
/// 这两种语义要分清 —— 混淆会让「只改昵称」意外把头像清掉。
|
||||
Future<AuthResponse> updateProfile({String? avatar, bool clearAvatar = false}) async {
|
||||
try {
|
||||
final res = await _dio.put('/auth/profile', data: {
|
||||
'avatar': ?avatar,
|
||||
if (clearAvatar) 'clearAvatar': true,
|
||||
});
|
||||
return AuthResponse.fromJson(_map(res.data));
|
||||
} on DioException catch (e) {
|
||||
throw _unwrap(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传头像,返回**相对路径**(再由 updateProfile 提交)。
|
||||
Future<String> uploadAvatar(String filePath) async {
|
||||
try {
|
||||
final form = FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(filePath, filename: _fileName(filePath)),
|
||||
});
|
||||
final res = await _dio.post<dynamic>(
|
||||
'/auth/avatar',
|
||||
data: form,
|
||||
options: Options(receiveTimeout: const Duration(seconds: 60)),
|
||||
);
|
||||
final data = res.data;
|
||||
if (data is Map && data['path'] != null) {
|
||||
return data['path'].toString();
|
||||
}
|
||||
throw ApiException(-1, '上传失败:服务端没返回路径');
|
||||
} on DioException catch (e) {
|
||||
throw _unwrap(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 从路径取文件名,后端靠扩展名判断类型
|
||||
static String _fileName(String path) {
|
||||
final normalized = path.replaceAll('\\', '/');
|
||||
final name = normalized.substring(normalized.lastIndexOf('/') + 1);
|
||||
return name.isEmpty ? 'avatar.jpg' : name;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _map(Object? data) {
|
||||
if (data is Map<String, dynamic>) return data;
|
||||
throw ApiException(-1, '响应格式异常');
|
||||
|
||||
Reference in New Issue
Block a user