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:
MingNian
2026-07-28 15:03:38 +08:00
parent 25a4078688
commit b4bc15b9b9
88 changed files with 5527 additions and 1240 deletions

View File

@@ -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();