feat: 文件存储安全加固 + 认证增强 + 媒体URL保护 + provider 重构
## 后端安全加固 - 新增 UserUploadPathResolver: 用户上传文件路径安全解析, 防目录穿越 - LocalReportFileStorage: 文件存储路径安全加固 - local_account_file_cleanup: 账号删除时文件清理逻辑增强 - AuthService: 认证逻辑增强 - file_endpoints / report_endpoints: 文件访问接口安全加固 - ai_chat_endpoints / doctor_endpoints: 接口安全调整 - Program.cs: 服务注册调整 ## 前端认证与媒体 - 新增 authenticated_network_image.dart: 带认证的图片加载组件 - auth_provider: 认证状态管理大幅增强(+173) - api_client: 网络客户端增强(+124) - chat_provider: 聊天 provider 重构(+76) - omron_device_provider: 蓝牙设备 provider 增强(+53) - sse_handler: SSE 处理增强(+35) - consultation_provider / data_providers / conversation_history_provider: 调整 ## 页面调整 - remaining_pages: 健康档案/饮食记录等页面增强(+115) - home_page / chat_messages_view: 主页微调 - doctor 端多页微调(consultations/dashboard/followups/patient_detail/profile/report_detail/reports) - report_pages / settings_pages / notification_prefs_page: 微调 - device_scan_page / diet_capture_page / admin_home_page: 微调 ## 测试 - 新增 file_path_security_tests: 文件路径安全测试 - 新增 protected_media_url_test: 媒体URL保护测试 - 新增 user_session_identity_test: 用户会话身份测试 - account_deletion_tests / application_service_tests / auth_tests: 更新
This commit is contained in:
@@ -86,10 +86,12 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
ActiveAgent? _lastTriggeredAgent;
|
||||
Timer? _agentTapLockTimer;
|
||||
bool _loadingConversation = false;
|
||||
int _generation = 0;
|
||||
|
||||
/// 重置整个会话:取消正在进行的 SSE,清空消息和会话 ID。
|
||||
/// 历史记录页一键清空 / 删除当前会话时调用。
|
||||
Future<void> resetSession() async {
|
||||
_generation++;
|
||||
await _cancelActiveStream();
|
||||
_cancelPendingAgentWelcome();
|
||||
_lastTriggeredAgent = null;
|
||||
@@ -140,7 +142,9 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
|
||||
@override
|
||||
ChatState build() {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
ref.onDispose(() {
|
||||
_generation++;
|
||||
_subscription?.cancel();
|
||||
_agentTapLockTimer?.cancel();
|
||||
_subscription = null;
|
||||
@@ -172,10 +176,13 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
}
|
||||
|
||||
Future<String?> loadConversation(String convId) async {
|
||||
if (state.isStreaming) return '小脉正在回复,请稍后再切换对话';
|
||||
if (_loadingConversation) return '正在加载其他对话,请稍候';
|
||||
_loadingConversation = true;
|
||||
await _cancelActiveStream();
|
||||
if (state.isStreaming) {
|
||||
await stopGenerating();
|
||||
} else {
|
||||
await _cancelActiveStream();
|
||||
}
|
||||
_cancelPendingAgentWelcome();
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
@@ -194,7 +201,9 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
role: role,
|
||||
content: map['content']?.toString() ?? '',
|
||||
createdAt:
|
||||
DateTime.tryParse(map['createdAt']?.toString() ?? '') ??
|
||||
DateTime.tryParse(
|
||||
map['createdAt']?.toString() ?? '',
|
||||
)?.toLocal() ??
|
||||
DateTime.now(),
|
||||
type: _messageTypeFromMetadata(metadata),
|
||||
metadata: metadata,
|
||||
@@ -258,7 +267,8 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
Future<void> sendImage(String imagePath, String text) async {
|
||||
if (state.isStreaming) return;
|
||||
final file = File(imagePath);
|
||||
if (!await file.exists()) return;
|
||||
if (!await file.exists() || state.isStreaming) return;
|
||||
final generation = ++_generation;
|
||||
_lastTriggeredAgent = null;
|
||||
_cancelPendingAgentWelcome();
|
||||
_resumeConversationFromHistory();
|
||||
@@ -286,6 +296,8 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
uploadError = e;
|
||||
}
|
||||
|
||||
if (generation != _generation || !state.isStreaming) return;
|
||||
|
||||
// 更新消息元数据(保留本地路径 + 添加远程URL)
|
||||
final updatedMsgs = state.messages.toList();
|
||||
final idx = updatedMsgs.indexWhere((m) => m.id == userMsg.id);
|
||||
@@ -318,14 +330,15 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
|
||||
// 把图片 URL 透传给后端,后端会调 VLM 识图并把描述拼到 LLM 上下文
|
||||
final userText = text.isNotEmpty ? text : '请帮我看看这张图片';
|
||||
await _sendToAI(userText, imageUrl: uploadedUrl);
|
||||
await _sendToAI(generation, userText, imageUrl: uploadedUrl);
|
||||
}
|
||||
|
||||
/// 发送 PDF 附件 + 文字(PDF 解析在后端做)。
|
||||
Future<void> sendPdf(String pdfPath, String fileName, String text) async {
|
||||
if (state.isStreaming) return;
|
||||
final file = File(pdfPath);
|
||||
if (!await file.exists()) return;
|
||||
if (!await file.exists() || state.isStreaming) return;
|
||||
final generation = ++_generation;
|
||||
_lastTriggeredAgent = null;
|
||||
_cancelPendingAgentWelcome();
|
||||
_resumeConversationFromHistory();
|
||||
@@ -350,6 +363,8 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
// ignore,下方统一处理
|
||||
}
|
||||
|
||||
if (generation != _generation || !state.isStreaming) return;
|
||||
|
||||
// 更新消息附带的远程 URL
|
||||
if (uploadedUrl != null) {
|
||||
final updatedMsgs = state.messages.toList();
|
||||
@@ -376,11 +391,12 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
return;
|
||||
}
|
||||
|
||||
await _sendToAI(userMsg.content, pdfUrl: uploadedUrl);
|
||||
await _sendToAI(generation, userMsg.content, pdfUrl: uploadedUrl);
|
||||
}
|
||||
|
||||
Future<void> sendMessage(String text) async {
|
||||
if (text.trim().isEmpty || state.isStreaming) return;
|
||||
final generation = ++_generation;
|
||||
_lastTriggeredAgent = null;
|
||||
_cancelPendingAgentWelcome();
|
||||
_resumeConversationFromHistory();
|
||||
@@ -396,14 +412,16 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
isStreaming: true,
|
||||
);
|
||||
|
||||
await _sendToAI(text);
|
||||
await _sendToAI(generation, text);
|
||||
}
|
||||
|
||||
Future<void> _sendToAI(
|
||||
int generation,
|
||||
String text, {
|
||||
String? imageUrl,
|
||||
String? pdfUrl,
|
||||
}) async {
|
||||
if (generation != _generation || !state.isStreaming) return;
|
||||
final aiMsg = ChatMessage(
|
||||
id: '${DateTime.now().millisecondsSinceEpoch}_ai',
|
||||
role: 'assistant',
|
||||
@@ -423,6 +441,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
_addError(aiMsg, '未登录,请重新登录');
|
||||
return;
|
||||
}
|
||||
if (generation != _generation || !state.isStreaming) return;
|
||||
|
||||
// 始终用 unified 智能体,AI 自动判断意图分配工具
|
||||
final stream = SseHandler.connect(
|
||||
@@ -435,12 +454,18 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
);
|
||||
|
||||
await _cancelActiveStream();
|
||||
if (generation != _generation || !state.isStreaming) return;
|
||||
|
||||
final done = Completer<void>();
|
||||
_streamDone = done;
|
||||
_subscription = stream.listen(
|
||||
(event) => _processEvent(event, aiMsg),
|
||||
(event) {
|
||||
if (generation == _generation) _processEvent(event, aiMsg);
|
||||
},
|
||||
onError: (_) {
|
||||
_addError(aiMsg, '网络异常,请稍后重试');
|
||||
if (generation == _generation) {
|
||||
_addError(aiMsg, '网络异常,请稍后重试');
|
||||
}
|
||||
if (!done.isCompleted) done.complete();
|
||||
},
|
||||
onDone: () {
|
||||
@@ -454,11 +479,13 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
_subscription = null;
|
||||
}
|
||||
|
||||
if (state.isStreaming) {
|
||||
if (generation == _generation && state.isStreaming) {
|
||||
_done(aiMsg);
|
||||
}
|
||||
} catch (e) {
|
||||
_addError(aiMsg, '网络异常,请稍后重试');
|
||||
if (generation == _generation) {
|
||||
_addError(aiMsg, '网络异常,请稍后重试');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,6 +503,31 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
_streamDone = null;
|
||||
}
|
||||
|
||||
Future<void> stopGenerating() async {
|
||||
if (!state.isStreaming) return;
|
||||
_generation++;
|
||||
await _cancelActiveStream();
|
||||
final messages = state.messages.toList();
|
||||
if (messages.isNotEmpty &&
|
||||
!messages.last.isUser &&
|
||||
messages.last.type == MessageType.text) {
|
||||
final last = messages.last;
|
||||
if (last.content.trim().isEmpty) {
|
||||
messages.removeLast();
|
||||
} else {
|
||||
last.content = '${last.content.trimRight()}\n\n(已停止生成)';
|
||||
last.metadata = {...?last.metadata, 'generationStopped': true};
|
||||
messages[messages.length - 1] = last;
|
||||
}
|
||||
}
|
||||
state = state.copyWith(
|
||||
messages: messages,
|
||||
isStreaming: false,
|
||||
thinkingText: null,
|
||||
);
|
||||
ref.invalidate(conversationHistoryProvider);
|
||||
}
|
||||
|
||||
void _addError(ChatMessage aiMsg, String errorText) {
|
||||
aiMsg.content = errorText;
|
||||
aiMsg.type = MessageType.text;
|
||||
|
||||
Reference in New Issue
Block a user