feat: 问诊对话+建档引导+报告+日历+趋势页重写+视觉统一
- 问诊对话页: 新建consultation_provider, 完整AI分身聊天UI - 首次建档引导: OnboardingPrompt+ManageArchiveTool+前端卡片 - 报告模块: POST端点 + report_agent_handler接VLM - 健康日历: GET /api/calendar端点 + 前端接API - 趋势页重写: 删除Mock数据 + 真实API + 手动录入 - 饮食保存: submit按钮接后端API - 侧边栏: 健康概览横排 + 统一紫色配色 - 运动计划: 修复JSON反序列化(record→手动解析) - VLM: prompt优化 + 模型切qwen3-vl-plus + 1280px压缩 - UI颜色: 加深主色 8B9CF7→6C5CE7 - 个人信息页精简 + 健康档案可编辑重设计 - 保洁: 删除无用agent_bar + 删除意见反馈/关于我们 - 新增AI提示词文档
This commit is contained in:
@@ -61,4 +61,73 @@ public static class CommonAgentHandler
|
||||
archive.Allergies, archive.DietRestrictions, archive.ChronicDiseases, archive.FamilyHistory,
|
||||
};
|
||||
}
|
||||
|
||||
public static readonly ToolDefinition ManageArchiveTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "manage_archive", Description = "管理用户健康档案",
|
||||
Parameters = new
|
||||
{
|
||||
type = "object",
|
||||
properties = new
|
||||
{
|
||||
action = new { type = "string", description = "update_diagnosis/update_surgery/update_allergies/update_chronic_diseases/update_diet_restrictions/query" },
|
||||
diagnosis = new { type = "string" },
|
||||
surgery_type = new { type = "string" },
|
||||
surgery_date = new { type = "string" },
|
||||
allergies = new { type = "array", items = new { type = "string" } },
|
||||
chronic_diseases = new { type = "array", items = new { type = "string" } },
|
||||
diet_restrictions = new { type = "array", items = new { type = "string" } },
|
||||
},
|
||||
required = new[] { "action" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static async Task<object> ExecuteManageArchive(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var action = args.TryGetProperty("action", out var a) ? a.GetString()! : "query";
|
||||
var archive = await db.HealthArchives.FirstOrDefaultAsync(x => x.UserId == userId);
|
||||
if (archive == null)
|
||||
{
|
||||
archive = new HealthArchive { Id = Guid.NewGuid(), UserId = userId };
|
||||
db.HealthArchives.Add(archive);
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "update_diagnosis":
|
||||
archive.Diagnosis = args.TryGetProperty("diagnosis", out var d) ? d.GetString() : archive.Diagnosis;
|
||||
break;
|
||||
case "update_surgery":
|
||||
archive.SurgeryType = args.TryGetProperty("surgery_type", out var st) ? st.GetString() : archive.SurgeryType;
|
||||
if (args.TryGetProperty("surgery_date", out var sd))
|
||||
archive.SurgeryDate = DateOnly.TryParse(sd.GetString(), out var date) ? date : archive.SurgeryDate;
|
||||
break;
|
||||
case "update_allergies":
|
||||
if (args.TryGetProperty("allergies", out var al) && al.ValueKind == JsonValueKind.Array)
|
||||
archive.Allergies = [.. al.EnumerateArray().Select(x => x.GetString()!)];
|
||||
break;
|
||||
case "update_chronic_diseases":
|
||||
if (args.TryGetProperty("chronic_diseases", out var cd) && cd.ValueKind == JsonValueKind.Array)
|
||||
archive.ChronicDiseases = [.. cd.EnumerateArray().Select(x => x.GetString()!)];
|
||||
break;
|
||||
case "update_diet_restrictions":
|
||||
if (args.TryGetProperty("diet_restrictions", out var dr) && dr.ValueKind == JsonValueKind.Array)
|
||||
archive.DietRestrictions = [.. dr.EnumerateArray().Select(x => x.GetString()!)];
|
||||
break;
|
||||
default: // query
|
||||
return new
|
||||
{
|
||||
found = true, archive.Diagnosis, archive.SurgeryType,
|
||||
SurgeryDate = archive.SurgeryDate?.ToString("yyyy-MM-dd"),
|
||||
archive.Allergies, archive.DietRestrictions, archive.ChronicDiseases, archive.FamilyHistory,
|
||||
};
|
||||
}
|
||||
|
||||
archive.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
return new { success = true };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,39 @@ public static class ReportAgentHandler
|
||||
_ => Task.FromResult<object>(new { success = false, message = $"未知工具: {toolName}" })
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<object> AnalyzeReport(AppDbContext db, Guid userId, JsonElement args, VisionClient visionClient)
|
||||
{
|
||||
var imageUrl = args.TryGetProperty("image_url", out var u) ? u.GetString()! : "";
|
||||
if (string.IsNullOrEmpty(imageUrl))
|
||||
return new { success = false, message = "缺少报告图片" };
|
||||
|
||||
var prompt = """
|
||||
你是一个医学报告解读专家。请分析以下检查报告图片,以JSON格式返回:
|
||||
{
|
||||
"reportType": "报告类型(血常规/生化全项/心电图/彩超/出院小结/其他)",
|
||||
"indicators": [
|
||||
{"name":"指标名","value":"数值","unit":"单位","range":"参考范围","status":"normal/high/low"}
|
||||
],
|
||||
"summary": "初步分析摘要",
|
||||
"needsDoctorReview": true/false
|
||||
}
|
||||
只返回JSON,不要其他内容。
|
||||
""";
|
||||
|
||||
var response = await visionClient.VisionAsync(prompt, [imageUrl], userText: "请分析这份检查报告");
|
||||
var content = response.Choices?.FirstOrDefault()?.Message?.Content ?? "{}";
|
||||
|
||||
// 保存到报告记录
|
||||
var report = await db.Reports
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.FirstOrDefaultAsync(r => r.UserId == userId && r.Status == ReportStatus.PendingDoctor);
|
||||
if (report != null)
|
||||
{
|
||||
report.AiSummary = content;
|
||||
report.AiIndicators = content;
|
||||
}
|
||||
|
||||
return new { success = true, analysis = content };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ public sealed class VisionClient(HttpClient http, IConfiguration config)
|
||||
throw new HttpRequestException($"VLM API {response.StatusCode}: {errorBody}");
|
||||
}
|
||||
var body = await response.Content.ReadAsStringAsync(ct);
|
||||
Console.WriteLine($"[VLM RAW] Model={_model}, BodyLen={body.Length}, Body={body[..Math.Min(body.Length, 500)]}");
|
||||
return JsonSerializer.Deserialize<ChatCompletionResponse>(body, _jsonOptions)!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ public sealed class PromptManager
|
||||
AgentType.Medication => MedicationPrompt,
|
||||
AgentType.Report => ReportPrompt,
|
||||
AgentType.Exercise => ExercisePrompt,
|
||||
AgentType.Onboarding => OnboardingPrompt,
|
||||
_ => DefaultPrompt
|
||||
};
|
||||
|
||||
@@ -115,4 +116,24 @@ public sealed class PromptManager
|
||||
4. 运动强度要循序渐进
|
||||
5. 避免剧烈运动
|
||||
""";
|
||||
|
||||
private const string OnboardingPrompt = """
|
||||
你是一个健康管家助手,正在帮助新用户建立健康档案。
|
||||
|
||||
必须严格遵守以下流程,一次只问一个问题:
|
||||
|
||||
步骤1:询问主要诊断(冠心病/高血压/糖尿病/其他)
|
||||
步骤2:询问手术史(PCI支架植入/心脏搭桥/其他/没有)→ 如果用户说有手术,追问手术日期
|
||||
步骤3:询问过敏史(青霉素/头孢/海鲜/鸡蛋/无过敏)→ 可多选
|
||||
步骤4:询问慢性病史(高血脂/糖尿病/痛风/无)
|
||||
步骤5:询问饮食限制(低盐/低脂/低糖/无限制)
|
||||
|
||||
规则:
|
||||
- 每次只问一个问题,给出2-4个快捷选项让用户点击
|
||||
- 用户回答每个问题后,立即调用 manage_archive 工具保存
|
||||
- 用户说"跳过"/"以后再说"/"再说"/"不用了"→ 礼貌结束建档:"好的,随时可以跟我说'完善档案'来继续"
|
||||
- 全部完成后,生成结构化总结卡片
|
||||
- 回复语气温暖、像朋友一样关怀患者
|
||||
- 每步只问一个问题不要多问
|
||||
""";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user