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

@@ -12,19 +12,23 @@ public static class MedicationAgentHandler
Function = new()
{
Name = "manage_medication",
Description = "用药管理(创建/查询/确认服药)",
Description = "用药管理(创建计划、按日期查询计划与逐顿服药状态、确认某一顿服药)",
Parameters = new
{
type = "object",
properties = new
{
action = new { type = "string", description = "create/query/confirm" },
action = new { type = "string", @enum = new[] { "create", "query", "confirm" }, description = "create/query/confirm" },
scope = new { type = "string", @enum = new[] { "today", "scheduled_date", "current_plans", "upcoming_plans", "ended_plans", "inactive_plans", "all_plans" }, description = "查询时必填:今日安排/指定日期安排/当前计划/即将开始/自然结束/已停用/全部计划" },
date = new { type = "string", description = "仅 scope=scheduled_date 时必填,使用北京时间日期 yyyy-MM-dd" },
medication_id = new { type = "string", description = "药品 ID确认服药时使用" },
scheduled_time = new { type = "string", description = "要确认的计划服药时间 HH:mm" },
name = new { type = "string", description = "药品名称" },
dosage = new { type = "string", description = "剂量,如 100mg" },
frequency = new { type = "string", description = "频率,如 Daily/EveryOtherDay/Weekly" },
frequency = new { type = "string", @enum = new[] { "Daily", "TwiceDaily", "ThreeTimesDaily", "EveryOtherDay", "Weekly", "AsNeeded" }, description = "服药频率" },
time_of_day = new { type = "array", items = new { type = "string" }, description = "服药时间,如 [\"08:00\",\"20:00\"]" },
duration_days = new { type = "integer", description = "服用天数" },
long_term = new { type = "boolean", description = "是否明确为长期服用;与 duration_days 二选一" },
start_date = new { type = "string", description = "开始日期 yyyy-MM-dd" },
},
required = new[] { "action" }
@@ -49,11 +53,11 @@ public static class MedicationAgentHandler
private static async Task<object> ExecuteManageMedication(IMedicationService medications, Guid userId, JsonElement args, CancellationToken ct)
{
var action = args.TryGetProperty("action", out var a) ? a.GetString()! : "query";
var action = args.TryGetProperty("action", out var a) ? a.GetString()?.ToLowerInvariant() : null;
return action switch
{
"create" => await CreateMedication(medications, userId, args, ct),
"query" => await QueryMedications(medications, userId, ct),
"query" => await QueryMedications(medications, userId, args, ct),
"confirm" => await ConfirmMedication(medications, userId, args, ct),
_ => new { success = false, message = $"未知操作: {action}" }
};
@@ -61,14 +65,19 @@ public static class MedicationAgentHandler
private static async Task<object> CreateMedication(IMedicationService medications, Guid userId, JsonElement args, CancellationToken ct)
{
var validationError = ValidateWriteArguments(args);
if (validationError != null)
return new { success = false, message = validationError };
var name = args.TryGetProperty("name", out var n) ? n.GetString()! : "";
var dosage = args.TryGetProperty("dosage", out var dg) ? dg.GetString() : null;
var frequency = ReadFrequency(args);
var times = ReadTimes(args);
var durationDays = args.TryGetProperty("duration_days", out var dd) ? dd.GetInt32() : 0;
var longTerm = args.TryGetProperty("long_term", out var longTermValue) && longTermValue.ValueKind == JsonValueKind.True;
var startDate = ReadStartDate(args);
// 结束日为包含式IsActiveOn 用 EndDate >= date 判断),故 N 天疗程结束日 = 起始日 + (N-1)
var endDate = durationDays > 0 ? startDate.AddDays(durationDays - 1) : (DateOnly?)null;
DateOnly? endDate = longTerm ? null : startDate.AddDays(durationDays - 1);
var medicationId = await medications.CreateAsync(userId, new MedicationUpsertRequest(
name,
@@ -91,27 +100,146 @@ public static class MedicationAgentHandler
time = timeLabels,
start_date = startDate.ToString("yyyy-MM-dd"),
duration_days = durationDays,
long_term = longTerm,
};
}
private static async Task<object> QueryMedications(IMedicationService medications, Guid userId, CancellationToken ct)
private static async Task<object> QueryMedications(
IMedicationService medications,
Guid userId,
JsonElement args,
CancellationToken ct)
{
var meds = await medications.ListActiveAsync(userId, ct);
return new { count = meds.Count, medications = meds.Select(m => new { m.Id, m.Name, m.Dosage, m.TimeOfDay }) };
var scope = args.TryGetProperty("scope", out var scopeValue)
? scopeValue.GetString()?.ToLowerInvariant()
: null;
if (scope is not ("today" or "scheduled_date" or "current_plans" or "upcoming_plans" or "ended_plans" or "inactive_plans" or "all_plans"))
return new
{
success = false,
message = "查询用药时必须明确 scopetoday、scheduled_date、current_plans、upcoming_plans、ended_plans、inactive_plans 或 all_plans",
};
if (scope == "scheduled_date" &&
(!args.TryGetProperty("date", out var requestedDate) || !DateOnly.TryParse(requestedDate.GetString(), out _)))
return new { success = false, message = "查询指定日期的用药安排时必须提供有效的 date" };
var date = scope == "scheduled_date"
? DateOnly.Parse(args.GetProperty("date").GetString()!)
: AiDateTime.BeijingToday;
var isDateScope = scope is "today" or "scheduled_date";
var overview = await medications.GetAiOverviewAsync(userId, date, ct);
var plans = scope switch
{
"all_plans" => overview.ToList(),
"current_plans" => overview.Where(plan => plan.Phase == "active").ToList(),
"upcoming_plans" => overview
.Where(plan => plan.Phase == "upcoming")
.OrderBy(plan => plan.StartDate)
.ToList(),
"ended_plans" => overview
.Where(plan => plan.Phase == "ended")
.OrderByDescending(plan => plan.EndDate)
.ToList(),
"inactive_plans" => overview
.Where(plan => plan.Phase == "inactive")
.OrderByDescending(plan => plan.EndDate)
.ToList(),
_ => overview.Where(plan => plan.ScheduledOnDate).ToList(),
};
var doseStatuses = plans.SelectMany(plan => plan.Doses).ToList();
var nextPlan = overview
.Where(plan => plan.Phase == "upcoming" && plan.StartDate.HasValue)
.OrderBy(plan => plan.StartDate)
.FirstOrDefault();
return new
{
query_date = date.ToString("yyyy-MM-dd"),
scope,
count = plans.Count,
all_doses_completed = isDateScope
? doseStatuses.Count > 0 && doseStatuses.All(dose => dose.Status == "taken")
: (bool?)null,
has_upcoming_plans = nextPlan != null,
next_start_date = nextPlan?.StartDate?.ToString("yyyy-MM-dd"),
days_until_next_start = nextPlan?.StartDate is DateOnly nextStart
? nextStart.DayNumber - date.DayNumber
: (int?)null,
plans,
};
}
private static async Task<object> ConfirmMedication(IMedicationService medications, Guid userId, JsonElement args, CancellationToken ct)
{
var medId = args.TryGetProperty("medication_id", out var mid) ? mid.GetGuid() : Guid.Empty;
var success = await medications.ConfirmNowAsync(userId, medId, ct);
return success ? new { success = true } : new { success = false, message = "药品不存在或今天已经打卡" };
var validationError = ValidateWriteArguments(args);
if (validationError != null)
return new { success = false, message = validationError };
var medId = args.TryGetProperty("medication_id", out var mid) && mid.TryGetGuid(out var parsedMedicationId)
? parsedMedicationId
: Guid.Empty;
if (!args.TryGetProperty("scheduled_time", out var timeValue) ||
!TimeOnly.TryParse(timeValue.GetString(), out var scheduledTime))
return new { success = false, message = "缺少要确认的计划服药时间" };
var success = await medications.ConfirmDoseAsync(
userId,
medId,
scheduledTime,
MedicationLogStatus.Taken,
ct);
return success == true
? new { success = true, medication_id = medId, scheduled_time = scheduledTime.ToString("HH:mm") }
: new { success = false, message = success == null ? "药品不存在" : "该时间不属于今天的服药安排,或这一顿已经记录" };
}
public static string? ValidateWriteArguments(JsonElement args)
{
var action = args.TryGetProperty("action", out var actionValue)
? actionValue.GetString()?.ToLowerInvariant()
: null;
if (action == "confirm")
{
if (!args.TryGetProperty("medication_id", out var medicationId) || !medicationId.TryGetGuid(out _))
return "确认服药时缺少有效的药品 ID";
if (!args.TryGetProperty("scheduled_time", out var scheduledTime) || !TimeOnly.TryParse(scheduledTime.GetString(), out _))
return "确认服药时缺少具体的计划服药时间";
return null;
}
if (action != "create") return null;
if (!args.TryGetProperty("name", out var name) || string.IsNullOrWhiteSpace(name.GetString()))
return "创建用药计划前需要确认药品名称";
if (!args.TryGetProperty("dosage", out var dosage) || string.IsNullOrWhiteSpace(dosage.GetString()))
return "创建用药计划前需要确认每次剂量";
if (!args.TryGetProperty("frequency", out var frequencyValue) ||
!Enum.TryParse<MedicationFrequency>(frequencyValue.GetString(), ignoreCase: true, out _))
return "创建用药计划前需要确认服药频率";
if (!args.TryGetProperty("time_of_day", out var times) || times.ValueKind != JsonValueKind.Array)
return "创建用药计划前需要确认具体服药时间";
var timeValues = times.EnumerateArray().ToList();
if (timeValues.Count == 0 || timeValues.Any(value => !TimeOnly.TryParse(value.GetString(), out _)))
return "创建用药计划前需要提供有效的具体服药时间";
if (!args.TryGetProperty("start_date", out var startDate) || !DateOnly.TryParse(startDate.GetString(), out _))
return "创建用药计划前需要确认开始日期";
var durationDays = 0;
var hasDuration = args.TryGetProperty("duration_days", out var duration) &&
duration.TryGetInt32(out durationDays) && durationDays > 0;
var isLongTerm = args.TryGetProperty("long_term", out var longTerm) && longTerm.ValueKind == JsonValueKind.True;
if (hasDuration && durationDays > 3650)
return "服用天数不能超过3650天长期服用请明确设置 long_term=true";
if (hasDuration && isLongTerm)
return "服用天数和长期服用不能同时设置";
if (!hasDuration && !isLongTerm)
return "创建用药计划前需要确认服用天数,或明确说明长期服用";
return null;
}
private static MedicationFrequency ReadFrequency(JsonElement args)
{
var frequencyStr = args.TryGetProperty("frequency", out var f) ? f.GetString() : "Daily";
return Enum.TryParse<MedicationFrequency>(frequencyStr, out var frequency) ? frequency : MedicationFrequency.Daily;
return Enum.TryParse<MedicationFrequency>(frequencyStr, ignoreCase: true, out var frequency) ? frequency : MedicationFrequency.Daily;
}
private static DateOnly ReadStartDate(JsonElement args)