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:
MingNian
2026-07-20 10:19:01 +08:00
parent 0d4fd88ce7
commit 9cea41705e
48 changed files with 1181 additions and 212 deletions

View File

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