feat: AI Agent 处理器扩展 + 服药打卡确认链路 + AI 气泡配色微调 + 数据刷新 providers 抽离 + 多端页面细节调整

This commit is contained in:
MingNian
2026-07-20 17:12:00 +08:00
parent 9cea41705e
commit 0cb5b8e85a
45 changed files with 1435 additions and 276 deletions

View File

@@ -37,6 +37,7 @@ public static class AiChatEndpoints
FastGptKnowledgeClient knowledgeClient,
IAiToolExecutionService toolExecution,
IAiWriteConfirmationStore confirmations,
IMedicationService medications,
IAiConversationService conversations,
IAttachmentContextBuilder attachments,
IPatientContextService patientContexts,
@@ -139,7 +140,11 @@ public static class AiChatEndpoints
knowledgeSnippet = await knowledgeClient.SearchAsync(message, ct);
}
var enhancedSystem = systemPrompt + "\n\n当前患者信息\n" + patientContext;
var beijingNow = DateTime.UtcNow.AddHours(8);
var enhancedSystem = systemPrompt
+ $"\n\n系统当前北京时间{beijingNow:yyyy-MM-dd HH:mm:ss}UTC+8。所有今天/昨天/当前/下一次’均以此时间为准。"
+ "\n\n当前患者背景信息仅作医学背景个人计划和完成状态必须调用对应工具获取最新数据\n"
+ patientContext;
if (!string.IsNullOrWhiteSpace(knowledgeSnippet))
{
enhancedSystem += "\n\n知识库参考资料如与用户问题相关请优先采用\n" + knowledgeSnippet;
@@ -243,7 +248,7 @@ public static class AiChatEndpoints
try
{
toolResult = IsWriteToolCall(tc.Function.Name, tc.Function.Arguments)
? await PreparePendingWriteAsync(confirmations, userId.Value, tc.Function.Name, tc.Function.Arguments, ct)
? await PreparePendingWriteAsync(confirmations, medications, userId.Value, tc.Function.Name, tc.Function.Arguments, ct)
: await toolExecution.ExecuteAsync(tc.Function.Name, tc.Function.Arguments, userId.Value, ct);
}
catch (Exception ex)
@@ -559,6 +564,10 @@ public static class AiChatEndpoints
MedicationAgentHandler.ManageMedicationTool,
ExerciseAgentHandler.ManageExerciseTool,
CommonAgentHandler.CheckArchiveTool,
PatientReadAgentHandler.QueryDietRecordsTool,
PatientReadAgentHandler.QueryFollowUpsTool,
PatientReadAgentHandler.QueryReportsTool,
PatientReadAgentHandler.QueryNotificationsTool,
],
_ => CommonAgentHandler.Tools,
};
@@ -566,7 +575,6 @@ public static class AiChatEndpoints
private static bool IsWriteToolCall(string toolName, string arguments)
{
if (toolName == "record_health_data") return true;
if (toolName == "manage_archive") return GetToolAction(arguments) != "query";
if (toolName == "manage_medication") return GetToolAction(arguments) is "create" or "confirm";
if (toolName == "manage_exercise") return GetToolAction(arguments) is "create" or "checkin";
return false;
@@ -589,14 +597,39 @@ public static class AiChatEndpoints
private static async Task<object> PreparePendingWriteAsync(
IAiWriteConfirmationStore confirmations,
IMedicationService medications,
Guid userId,
string toolName,
string arguments,
CancellationToken ct)
{
var command = await confirmations.CreateAsync(userId, toolName, arguments, TimeSpan.FromMinutes(10), ct);
using var json = JsonDocument.Parse(arguments);
var args = json.RootElement;
AiMedicationPlanDto? medicationPlan = null;
if (toolName == "manage_medication")
{
var validationError = MedicationAgentHandler.ValidateWriteArguments(args);
if (validationError != null)
return new { success = false, message = validationError };
if (string.Equals(GetString(args, "action"), "confirm", StringComparison.OrdinalIgnoreCase))
{
var medicationId = args.GetProperty("medication_id").GetGuid();
var scheduledTime = GetString(args, "scheduled_time")!;
var overview = await medications.GetAiOverviewAsync(
userId,
DateOnly.FromDateTime(DateTime.UtcNow.AddHours(8)),
ct);
medicationPlan = overview.FirstOrDefault(plan =>
plan.Id == medicationId &&
plan.ScheduledOnDate &&
plan.Doses.Any(dose => dose.ScheduledTime == scheduledTime && dose.Status is "scheduled" or "overdue"));
if (medicationPlan == null)
return new { success = false, message = "没有找到这顿尚未确认的今日服药安排,请重新查询后再确认" };
}
}
var command = await confirmations.CreateAsync(userId, toolName, arguments, TimeSpan.FromMinutes(10), ct);
var preview = new Dictionary<string, object?>
{
@@ -613,12 +646,15 @@ public static class AiChatEndpoints
break;
case "manage_medication":
preview["type"] = "medication";
preview["name"] = GetString(args, "name") ?? "服药确认";
preview["dosage"] = GetString(args, "dosage") ?? "";
preview["frequency"] = GetString(args, "frequency") ?? "Daily";
preview["time"] = GetStringArray(args, "time_of_day");
var medicationAction = GetString(args, "action");
preview["name"] = medicationPlan?.Name ?? GetString(args, "name") ?? "用药计划";
preview["operation"] = medicationAction ?? "create";
preview["dosage"] = medicationPlan?.Dosage ?? GetString(args, "dosage") ?? "";
preview["frequency"] = medicationPlan?.Frequency ?? GetString(args, "frequency") ?? "";
preview["time"] = GetString(args, "scheduled_time") ?? GetStringArray(args, "time_of_day");
preview["duration_days"] = GetInt(args, "duration_days") ?? 0;
preview["start_date"] = GetString(args, "start_date") ?? DateOnly.FromDateTime(DateTime.UtcNow.AddHours(8)).ToString("yyyy-MM-dd");
preview["long_term"] = args.TryGetProperty("long_term", out var longTerm) && longTerm.ValueKind == JsonValueKind.True;
preview["start_date"] = medicationPlan?.StartDate?.ToString("yyyy-MM-dd") ?? GetString(args, "start_date") ?? "";
break;
case "manage_exercise":
preview["type"] = "exercise";
@@ -628,11 +664,6 @@ public static class AiChatEndpoints
preview["start_date"] = GetString(args, "start_date") ?? DateOnly.FromDateTime(DateTime.UtcNow.AddHours(8)).ToString("yyyy-MM-dd");
preview["reminder_time"] = GetString(args, "reminder_time") ?? "19:00";
break;
case "manage_archive":
preview["type"] = "archive";
preview["value"] = "健康档案更新";
preview["unit"] = "";
break;
}
return preview;
@@ -805,6 +836,8 @@ public static class AiChatEndpoints
{
if (resultDict.TryGetValue("name", out var name))
metadata["name"] = name?.ToString() ?? "";
if (resultDict.TryGetValue("operation", out var operation))
metadata["operation"] = operation?.ToString() ?? "create";
if (resultDict.TryGetValue("dosage", out var dosage))
metadata["dosage"] = dosage?.ToString() ?? "";
if (resultDict.TryGetValue("time", out var time))
@@ -834,15 +867,6 @@ public static class AiChatEndpoints
metadata["recordTime"] = DateTime.UtcNow.AddHours(8).ToString("MM月dd日 HH:mm");
}
break;
case "manage_archive":
if (!isPendingConfirmation) break;
messageType = "data_confirm";
metadata["type"] = "archive";
metadata["value"] = "健康档案更新";
metadata["unit"] = "";
metadata["success"] = true;
metadata["recordTime"] = DateTime.UtcNow.AddHours(8).ToString("MM月dd日 HH:mm");
break;
case "analyze_report":
messageType = "report_analysis";
break;

View File

@@ -42,15 +42,6 @@ public static class MedicationEndpoints
: Results.Ok(new { code = 40004, data = (object?)null, message = "用药记录不存在" });
});
group.MapPost("/{id:guid}/confirm", async (Guid id, HttpContext http, IMedicationService medications, CancellationToken ct) =>
{
var userId = GetUserId(http);
var taken = await medications.ToggleTodayTakenAsync(userId, id, ct);
if (taken == null) return Results.Ok(new { code = 404, message = "药品不存在" });
return Results.Ok(new { code = 0, data = new { taken }, message = (string?)null });
});
group.MapGet("/reminders", async (HttpContext http, IMedicationService medications, CancellationToken ct) =>
{
var userId = GetUserId(http);