feat: AI 对话附件上下文解析 + 历史会话归档 + 多页面 UI 重构
- 后端: 新增 AttachmentContextBuilder 解析图片/PDF 摘要并拼入 LLM 上下文; ai_chat_endpoints 扩展附件接口; 新增 ReportAnalysisService - 前端: 新增历史会话页与 conversation_history_provider; chat 链路支持附件展示与回放 - UI: 重构 medication_checkin / notification_center / profile / health_drawer 等多页面 - 配置: api_client baseUrl 适配当前 WiFi IP
This commit is contained in:
@@ -2,6 +2,7 @@ using Health.Application.Reports;
|
||||
using Health.Application.Notifications;
|
||||
using Health.Infrastructure.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using UglyToad.PdfPig;
|
||||
|
||||
namespace Health.Infrastructure.Reports;
|
||||
|
||||
@@ -17,6 +18,7 @@ public sealed class ReportAnalysisService(
|
||||
private readonly DeepSeekClient _llm = llm;
|
||||
private readonly IUserNotificationProducer _notifications = notifications;
|
||||
private readonly ILogger<ReportAnalysisService> _logger = logger;
|
||||
private const int MaxPdfChars = 8000;
|
||||
|
||||
public async Task AnalyzeAsync(ReportAnalysisJob job, CancellationToken ct)
|
||||
{
|
||||
@@ -72,6 +74,9 @@ public sealed class ReportAnalysisService(
|
||||
|
||||
private async Task<JsonElement> ExtractIndicatorsAsync(string filePath, CancellationToken ct)
|
||||
{
|
||||
if (Path.GetExtension(filePath).Equals(".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
return await ExtractIndicatorsFromPdfAsync(filePath, ct);
|
||||
|
||||
var imageUrl = await GetLocalImageUrl(filePath, ct);
|
||||
if (imageUrl == null)
|
||||
throw new InvalidOperationException("报告图片文件不存在或无法读取");
|
||||
@@ -112,6 +117,66 @@ public sealed class ReportAnalysisService(
|
||||
return json ?? throw new InvalidOperationException("报告识别结果格式异常");
|
||||
}
|
||||
|
||||
private async Task<JsonElement> ExtractIndicatorsFromPdfAsync(string filePath, CancellationToken ct)
|
||||
{
|
||||
var text = ExtractPdfText(filePath);
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return JsonDocument.Parse("""
|
||||
{"isReport": false, "reason": "这份 PDF 可能是扫描件或图片版,当前无法直接提取文字。请上传清晰图片,或后续启用 PDF 页转图片后再用 VLM 识别。"}
|
||||
""").RootElement;
|
||||
}
|
||||
|
||||
var prompt = $$"""
|
||||
你是一名医学检验报告分析专家。下面是用户上传 PDF 中提取出的文字。
|
||||
|
||||
PDF 文本:
|
||||
{{text}}
|
||||
|
||||
第一步:判断这到底是不是一份医学检验报告。
|
||||
如果不是,直接返回:
|
||||
{"isReport": false, "reason": "这不是医学报告,原因:..."}
|
||||
|
||||
如果是医学报告,提取所有检测指标,返回严格 JSON 格式(不要 markdown 包裹):
|
||||
{
|
||||
"isReport": true,
|
||||
"reportType": "BloodTest/Biochemistry/Ecg/Ultrasound/Discharge/Other",
|
||||
"indicators": [
|
||||
{"name": "指标名称", "value": "数值", "unit": "单位", "referenceRange": "参考范围", "status": "normal/high/low"}
|
||||
]
|
||||
}
|
||||
|
||||
注意:
|
||||
- 只提取报告里真实出现的指标
|
||||
- status 判断:数值在参考范围内=normal,超出上限=high,低于下限=low
|
||||
- 不确定的参考范围不要编造,可留空
|
||||
""";
|
||||
|
||||
var messages = new List<ChatMessage>
|
||||
{
|
||||
new() { Role = "system", Content = "你是医学报告结构化抽取助手,只输出严格 JSON。" },
|
||||
new() { Role = "user", Content = prompt }
|
||||
};
|
||||
|
||||
var response = await _llm.ChatAsync(messages, maxTokens: 1800, temperature: 0.1f, ct: ct);
|
||||
var content = response.Choices?.FirstOrDefault()?.Message?.Content ?? "";
|
||||
var json = TryParseJson(content);
|
||||
return json ?? throw new InvalidOperationException("PDF 报告解析结果格式异常");
|
||||
}
|
||||
|
||||
private static string ExtractPdfText(string filePath)
|
||||
{
|
||||
using var pdf = PdfDocument.Open(filePath);
|
||||
var sb = new System.Text.StringBuilder();
|
||||
foreach (var page in pdf.GetPages())
|
||||
{
|
||||
sb.AppendLine(page.Text);
|
||||
if (sb.Length > MaxPdfChars) break;
|
||||
}
|
||||
var text = sb.ToString().Trim();
|
||||
return text.Length > MaxPdfChars ? text[..MaxPdfChars] : text;
|
||||
}
|
||||
|
||||
private async Task<string> GenerateSummaryAsync(string indicatorsJson, CancellationToken ct)
|
||||
{
|
||||
var prompt = $"""
|
||||
|
||||
Reference in New Issue
Block a user