feat: AI 意图路由/草稿存储 + Prompts 模块化 + UI 视觉统一 + AI 同意门 + 资源更新
后端: - 新增 ai_intent_router 意图路由 - 新增 AiEntryDraft 草稿存储 + EF 迁移 - 新增 Prompts 模块化(global/modules/rag/router) - AI Agent handlers 扩展 前端: - 新增 AI 同意门 (ai_consent_gate/provider/details_page) - HealthMetricVisuals 视觉统一 - 设备扫描/管理页面优化 - agent 插图替换 + 趋势指标图标 配置: - DeepSeek 模型改 deepseek-v4-flash - .gitignore 加 artifacts/audit - build.gradle.kts 签名配置调整
This commit is contained in:
@@ -6,6 +6,7 @@ import 'auth_provider.dart';
|
||||
import 'conversation_history_provider.dart';
|
||||
import 'data_providers.dart';
|
||||
import 'data_refresh_providers.dart';
|
||||
import '../core/api_client.dart';
|
||||
import '../utils/sse_handler.dart';
|
||||
|
||||
enum MessageType { text, dataConfirm, agentWelcome, taskCard }
|
||||
@@ -104,6 +105,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
Timer? _agentTapLockTimer;
|
||||
bool _loadingConversation = false;
|
||||
int _generation = 0;
|
||||
final Set<String> _confirmingMessageIds = <String>{};
|
||||
|
||||
/// 重置整个会话:取消正在进行的 SSE,清空消息和会话 ID。
|
||||
/// 历史记录页一键清空 / 删除当前会话时调用。
|
||||
@@ -117,20 +119,23 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
|
||||
/// 不可变消息操作方法(供 chat_messages_view 新版代码调用)
|
||||
Future<String?> confirmMessage(String id) async {
|
||||
if (!_confirmingMessageIds.add(id)) return null;
|
||||
final msgs = state.messages.toList();
|
||||
final i = msgs.indexWhere((m) => m.id == id);
|
||||
if (i < 0) return '确认卡片不存在';
|
||||
if (msgs[i].isReadOnly) 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 {
|
||||
if (i < 0) return '确认卡片不存在';
|
||||
if (msgs[i].isReadOnly) return '历史记录中的录入卡片仅供查看';
|
||||
|
||||
final original = msgs[i];
|
||||
final metadata = _cloneMetadata(original.metadata) ?? <String, dynamic>{};
|
||||
final rawIds = metadata['confirmationIds'];
|
||||
final confirmationIds = rawIds is List
|
||||
? rawIds.map((e) => e.toString()).where((e) => e.isNotEmpty).toList()
|
||||
: <String>[];
|
||||
if (confirmationIds.isEmpty) {
|
||||
return '确认信息已失效,请重新发送需要录入的内容';
|
||||
}
|
||||
|
||||
final api = ref.read(apiClientProvider);
|
||||
for (final confirmationId in confirmationIds.toList()) {
|
||||
final response = await api.post(
|
||||
@@ -141,19 +146,24 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
return body is Map ? body['message']?.toString() ?? '录入失败' : '录入失败';
|
||||
}
|
||||
confirmationIds.remove(confirmationId);
|
||||
msgs[i].metadata?['confirmationIds'] = confirmationIds.toList();
|
||||
metadata['confirmationIds'] = List<String>.from(confirmationIds);
|
||||
msgs[i] = _copyMessage(original, metadata: metadata);
|
||||
state = state.copyWith(messages: msgs);
|
||||
}
|
||||
} catch (e) {
|
||||
return '录入失败,请检查网络后重试';
|
||||
}
|
||||
|
||||
msgs[i].confirmed = true;
|
||||
state = state.copyWith(messages: msgs);
|
||||
ref.read(medicationDataRefreshSignalProvider.notifier).trigger();
|
||||
ref.invalidate(latestHealthProvider);
|
||||
ref.read(exerciseDataRefreshSignalProvider.notifier).trigger();
|
||||
return null;
|
||||
msgs[i] = _copyMessage(original, metadata: metadata, confirmed: true);
|
||||
state = state.copyWith(messages: msgs);
|
||||
ref.read(medicationDataRefreshSignalProvider.notifier).trigger();
|
||||
ref.invalidate(latestHealthProvider);
|
||||
ref.read(exerciseDataRefreshSignalProvider.notifier).trigger();
|
||||
return null;
|
||||
} on ApiException catch (e) {
|
||||
return e.message;
|
||||
} catch (_) {
|
||||
return '录入失败,请检查网络后重试';
|
||||
} finally {
|
||||
_confirmingMessageIds.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -280,7 +290,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
|
||||
// 先显示用户消息(本地显示图片路径)
|
||||
final userMsg = ChatMessage(
|
||||
id: '${DateTime.now().millisecondsSinceEpoch}',
|
||||
id: '${DateTime.now().microsecondsSinceEpoch}',
|
||||
role: 'user',
|
||||
content: text.isNotEmpty ? text : '[图片]',
|
||||
createdAt: DateTime.now(),
|
||||
@@ -349,7 +359,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
_resumeConversationFromHistory();
|
||||
|
||||
final userMsg = ChatMessage(
|
||||
id: '${DateTime.now().millisecondsSinceEpoch}',
|
||||
id: '${DateTime.now().microsecondsSinceEpoch}',
|
||||
role: 'user',
|
||||
content: text.isNotEmpty ? text : '请帮我看看这份 PDF',
|
||||
createdAt: DateTime.now(),
|
||||
@@ -407,7 +417,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
_resumeConversationFromHistory();
|
||||
|
||||
final userMsg = ChatMessage(
|
||||
id: '${DateTime.now().millisecondsSinceEpoch}',
|
||||
id: '${DateTime.now().microsecondsSinceEpoch}',
|
||||
role: 'user',
|
||||
content: text,
|
||||
createdAt: DateTime.now(),
|
||||
@@ -428,7 +438,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
}) async {
|
||||
if (generation != _generation || !state.isStreaming) return;
|
||||
final aiMsg = ChatMessage(
|
||||
id: '${DateTime.now().millisecondsSinceEpoch}_ai',
|
||||
id: '${DateTime.now().microsecondsSinceEpoch}_ai',
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
createdAt: DateTime.now(),
|
||||
@@ -563,6 +573,15 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
aiMsg.content += (j['data'] as String?) ?? '';
|
||||
state = state.copyWith(thinkingText: null);
|
||||
_update(aiMsg);
|
||||
case 'confirmation':
|
||||
aiMsg.type = _parseMessageType(j['type'] as String? ?? 'data_confirm');
|
||||
aiMsg.confirmed = false;
|
||||
if (j['metadata'] is Map) {
|
||||
aiMsg.metadata = _cloneMetadata(
|
||||
Map<String, dynamic>.from(j['metadata']),
|
||||
);
|
||||
}
|
||||
_update(aiMsg);
|
||||
case 'notice':
|
||||
state = state.copyWith(thinkingText: j['message'] as String?);
|
||||
case 'tool_result':
|
||||
@@ -620,6 +639,27 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
state = state.copyWith(messages: u);
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _cloneMetadata(Map<String, dynamic>? metadata) {
|
||||
if (metadata == null) return null;
|
||||
return Map<String, dynamic>.from(jsonDecode(jsonEncode(metadata)) as Map);
|
||||
}
|
||||
|
||||
ChatMessage _copyMessage(
|
||||
ChatMessage source, {
|
||||
Map<String, dynamic>? metadata,
|
||||
bool? confirmed,
|
||||
}) {
|
||||
return ChatMessage(
|
||||
id: source.id,
|
||||
role: source.role,
|
||||
content: source.content,
|
||||
createdAt: source.createdAt,
|
||||
type: source.type,
|
||||
metadata: metadata ?? _cloneMetadata(source.metadata),
|
||||
confirmed: confirmed ?? source.confirmed,
|
||||
);
|
||||
}
|
||||
|
||||
void _done(ChatMessage m) {
|
||||
final u = state.messages.toList();
|
||||
final content = m.content.trim();
|
||||
|
||||
Reference in New Issue
Block a user