feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化
- 核心业务拆分为 Endpoint → Application Service → Repository 三层 - AI写入操作必须用户确认后才写库(确认卡片机制) - 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复) - 运动计划修复: 连续真实日期替代周模板 - 用药提醒去重 + 通知Outbox预留 - 认证收拢到AuthService, 管理员收拢到AdminService - AI会话加用户归属校验防串号 - 提示词调整为患者视角 - 开发假数据已关闭 - 21/21测试通过, 0警告0错误
This commit is contained in:
@@ -79,64 +79,60 @@ final chatProvider = NotifierProvider<ChatNotifier, ChatState>(
|
||||
ChatNotifier.new,
|
||||
);
|
||||
|
||||
ActiveAgent _parseAgent(String? type) {
|
||||
switch (type?.toLowerCase()) {
|
||||
case 'consultation':
|
||||
return ActiveAgent.consultation;
|
||||
case 'health':
|
||||
return ActiveAgent.health;
|
||||
case 'diet':
|
||||
return ActiveAgent.diet;
|
||||
case 'medication':
|
||||
return ActiveAgent.medication;
|
||||
case 'report':
|
||||
return ActiveAgent.report;
|
||||
case 'exercise':
|
||||
return ActiveAgent.exercise;
|
||||
default:
|
||||
return ActiveAgent.default_;
|
||||
}
|
||||
}
|
||||
|
||||
class ChatNotifier extends Notifier<ChatState> {
|
||||
StreamSubscription<Map<String, dynamic>>? _subscription;
|
||||
Completer<void>? _streamDone;
|
||||
ActiveAgent? _lastTriggeredAgent;
|
||||
|
||||
void markNeedsRebuild() => state = state.copyWith();
|
||||
|
||||
/// 不可变消息操作方法(供 chat_messages_view 新版代码调用)
|
||||
void confirmMessage(String id) {
|
||||
Future<String?> confirmMessage(String id) async {
|
||||
final msgs = state.messages.toList();
|
||||
final i = msgs.indexWhere((m) => m.id == id);
|
||||
if (i >= 0) {
|
||||
msgs[i].confirmed = true;
|
||||
state = state.copyWith(messages: msgs);
|
||||
if (i < 0) return '确认卡片不存在';
|
||||
|
||||
final rawIds = msgs[i].metadata?['confirmationIds'];
|
||||
final confirmationIds = rawIds is List
|
||||
? rawIds.map((e) => e.toString()).where((e) => e.isNotEmpty).toList()
|
||||
: <String>[];
|
||||
if (confirmationIds.isEmpty) {
|
||||
return '确认信息已失效,请重新发送需要录入的内容';
|
||||
}
|
||||
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
for (final confirmationId in confirmationIds.toList()) {
|
||||
final response = await api.post('/api/ai/confirm-write/$confirmationId');
|
||||
final body = response.data;
|
||||
if (body is! Map || body['code'] != 0) {
|
||||
return body is Map ? body['message']?.toString() ?? '录入失败' : '录入失败';
|
||||
}
|
||||
confirmationIds.remove(confirmationId);
|
||||
msgs[i].metadata?['confirmationIds'] = confirmationIds.toList();
|
||||
state = state.copyWith(messages: msgs);
|
||||
}
|
||||
} catch (e) {
|
||||
return '录入失败,请检查网络后重试';
|
||||
}
|
||||
|
||||
msgs[i].confirmed = true;
|
||||
state = state.copyWith(messages: msgs);
|
||||
ref.invalidate(medicationListProvider);
|
||||
ref.invalidate(latestHealthProvider);
|
||||
}
|
||||
|
||||
void startEditingField(String msgId, String fieldLabel) {
|
||||
final msgs = state.messages.toList();
|
||||
final i = msgs.indexWhere((m) => m.id == msgId);
|
||||
if (i >= 0) {
|
||||
msgs[i].metadata?['_editingField'] = fieldLabel;
|
||||
state = state.copyWith(messages: msgs);
|
||||
}
|
||||
}
|
||||
|
||||
void finishEditingField(String msgId, String fieldLabel, String value) {
|
||||
final msgs = state.messages.toList();
|
||||
final i = msgs.indexWhere((m) => m.id == msgId);
|
||||
if (i >= 0) {
|
||||
if (value.isNotEmpty) msgs[i].metadata?[fieldLabel] = value;
|
||||
msgs[i].metadata?.remove('_editingField');
|
||||
state = state.copyWith(messages: msgs);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
ChatState build() {
|
||||
ref.onDispose(() {
|
||||
_subscription?.cancel();
|
||||
_subscription = null;
|
||||
if (_streamDone != null && !_streamDone!.isCompleted) {
|
||||
_streamDone!.complete();
|
||||
}
|
||||
_streamDone = null;
|
||||
});
|
||||
Future.microtask(() {
|
||||
insertTaskCard();
|
||||
});
|
||||
@@ -162,7 +158,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
void setAgent(ActiveAgent a) {
|
||||
// 流式回复中忽略胶囊切换,防止状态混乱
|
||||
if (state.isStreaming) return;
|
||||
_subscription?.cancel();
|
||||
_cancelActiveStream();
|
||||
state = state.copyWith(activeAgent: a);
|
||||
ref.read(selectedAgentProvider.notifier).select(a);
|
||||
}
|
||||
@@ -198,7 +194,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
}
|
||||
|
||||
Future<void> loadConversation(String convId) async {
|
||||
_subscription?.cancel();
|
||||
await _cancelActiveStream();
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/ai/conversations/$convId');
|
||||
@@ -277,11 +273,12 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
|
||||
// 异步上传图片
|
||||
String? uploadedUrl;
|
||||
Object? uploadError;
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
uploadedUrl = await api.uploadFile('/api/files/upload', file);
|
||||
} catch (_) {
|
||||
// 上传失败:保留本地路径,仍然可以本地显示
|
||||
} catch (e) {
|
||||
uploadError = e;
|
||||
}
|
||||
|
||||
// 更新消息元数据(保留本地路径 + 添加远程URL)
|
||||
@@ -300,6 +297,17 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
state = state.copyWith(messages: updatedMsgs);
|
||||
}
|
||||
|
||||
if (uploadedUrl == null) {
|
||||
final errorMsg = ChatMessage(
|
||||
id: '${DateTime.now().millisecondsSinceEpoch}_upload_error',
|
||||
role: 'assistant',
|
||||
content: uploadError == null ? '图片上传失败,请稍后重试。' : '图片上传失败,请检查文件大小或网络后重试。',
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
state = state.copyWith(messages: [...state.messages, errorMsg]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 将图片 URL 作为消息内容发送给 AI
|
||||
final msgWithImage = text.isNotEmpty ? '$text\n[图片已上传]' : '[图片已上传]';
|
||||
await _sendToAI(msgWithImage);
|
||||
@@ -352,9 +360,26 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
token: token,
|
||||
);
|
||||
|
||||
await for (final event in stream) {
|
||||
_processEvent(event, aiMsg);
|
||||
await _cancelActiveStream();
|
||||
final done = Completer<void>();
|
||||
_streamDone = done;
|
||||
_subscription = stream.listen(
|
||||
(event) => _processEvent(event, aiMsg),
|
||||
onError: (_) {
|
||||
_addError(aiMsg, '网络异常,请稍后重试');
|
||||
if (!done.isCompleted) done.complete();
|
||||
},
|
||||
onDone: () {
|
||||
if (!done.isCompleted) done.complete();
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
await done.future;
|
||||
if (_streamDone == done) {
|
||||
_streamDone = null;
|
||||
_subscription = null;
|
||||
}
|
||||
|
||||
if (state.isStreaming) {
|
||||
_done(aiMsg);
|
||||
}
|
||||
@@ -363,6 +388,15 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cancelActiveStream() async {
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
if (_streamDone != null && !_streamDone!.isCompleted) {
|
||||
_streamDone!.complete();
|
||||
}
|
||||
_streamDone = null;
|
||||
}
|
||||
|
||||
void _addError(ChatMessage aiMsg, String errorText) {
|
||||
aiMsg.content = errorText;
|
||||
aiMsg.type = MessageType.text;
|
||||
|
||||
Reference in New Issue
Block a user