Files
teatea-pension/modules/ai/server/elder-context.ts

332 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import "server-only";
import { and, desc, eq } from "drizzle-orm";
import type { AiCitation, ElderAiDataScope } from "@/modules/ai/types";
import { getDatabase } from "@/modules/core/server/db";
import {
admissions,
alertTriggers,
beds,
careTasks,
chronicConditions,
elders,
familyFeedback,
healthAnomalyReviews,
healthProfiles,
rooms,
systemIncidents,
vitalRecords,
} from "@/modules/core/server/schema";
import type { AuthContext } from "@/modules/core/types";
type ElderAiContextResult =
| { success: true; context: ElderAiResidentContext }
| { success: false; reason: string; status: number };
export type ElderAiResidentContext = {
organizationId: string;
elderId: string;
elderName: string;
dataScopes: ElderAiDataScope[];
sections: string[];
citations: AiCitation[];
promptContext: string;
};
function hasPermission(context: AuthContext, permission: string): boolean {
return context.permissions.includes(permission as never);
}
function toIsoString(value: Date | null): string {
return value ? value.toISOString() : "";
}
function pushCitation(citations: AiCitation[], input: Omit<AiCitation, "id">): string {
const id = `ctx-${citations.length + 1}`;
citations.push({ id, ...input });
return id;
}
function compactText(value: string): string {
return value.trim().replace(/\s+/g, " ");
}
function appendSection(parts: string[], title: string, lines: string[]): void {
const compactLines = lines.map(compactText).filter(Boolean);
if (compactLines.length === 0) {
return;
}
parts.push(`## ${title}\n${compactLines.join("\n")}`);
}
export async function buildElderAiContext(context: AuthContext, elderId: string): Promise<ElderAiContextResult> {
const organizationId = context.organization?.id;
if (!organizationId) {
return { success: false, reason: "请选择机构后生成 AI 分析", status: 400 };
}
const database = getDatabase();
const elderRows = await database
.select()
.from(elders)
.where(and(eq(elders.id, elderId), eq(elders.organizationId, organizationId)))
.limit(1);
const elder = elderRows[0];
if (!elder) {
return { success: false, reason: "老人档案不存在", status: 404 };
}
const dataScopes: ElderAiDataScope[] = ["elder"];
const citations: AiCitation[] = [];
const sections: string[] = [];
const elderCitationId = pushCitation(citations, {
sourceType: "resident_context",
sourceId: elder.id,
title: "老人基础档案",
excerpt: `${elder.name}${elder.age} 岁,照护等级 ${elder.careLevel},状态 ${elder.status}`,
});
appendSection(sections, "老人基础档案", [
`[${elderCitationId}] 姓名:${elder.name};性别:${elder.gender};年龄:${elder.age};照护等级:${elder.careLevel};状态:${elder.status}`,
`主要联系人:${elder.primaryContact};电话:${elder.phone}`,
elder.medicalNotes ? `医疗备注:${elder.medicalNotes}` : "",
]);
const [
admissionRows,
healthProfileRows,
vitalRows,
conditionRows,
reviewRows,
careTaskRows,
alertRows,
feedbackRows,
incidentRows,
] = await Promise.all([
hasPermission(context, "admission:read") || hasPermission(context, "facility:read")
? database
.select({
admission: admissions,
bedCode: beds.code,
roomCode: rooms.code,
})
.from(admissions)
.leftJoin(beds, eq(beds.id, admissions.bedId))
.leftJoin(rooms, eq(rooms.id, beds.roomId))
.where(and(eq(admissions.organizationId, organizationId), eq(admissions.elderId, elderId)))
.orderBy(desc(admissions.admittedAt))
.limit(8)
: Promise.resolve([]),
hasPermission(context, "health:read")
? database
.select()
.from(healthProfiles)
.where(and(eq(healthProfiles.organizationId, organizationId), eq(healthProfiles.elderId, elderId)))
.limit(1)
: Promise.resolve([]),
hasPermission(context, "health:read")
? database
.select()
.from(vitalRecords)
.where(and(eq(vitalRecords.organizationId, organizationId), eq(vitalRecords.elderId, elderId)))
.orderBy(desc(vitalRecords.recordedAt))
.limit(8)
: Promise.resolve([]),
hasPermission(context, "health:read")
? database
.select()
.from(chronicConditions)
.where(and(eq(chronicConditions.organizationId, organizationId), eq(chronicConditions.elderId, elderId)))
.orderBy(desc(chronicConditions.updatedAt))
.limit(8)
: Promise.resolve([]),
hasPermission(context, "health:read")
? database
.select()
.from(healthAnomalyReviews)
.where(and(eq(healthAnomalyReviews.organizationId, organizationId), eq(healthAnomalyReviews.elderId, elderId)))
.orderBy(desc(healthAnomalyReviews.createdAt))
.limit(8)
: Promise.resolve([]),
hasPermission(context, "care:read")
? database
.select()
.from(careTasks)
.where(and(eq(careTasks.organizationId, organizationId), eq(careTasks.elderId, elderId)))
.orderBy(desc(careTasks.scheduledAt))
.limit(12)
: Promise.resolve([]),
hasPermission(context, "alert:read")
? database
.select()
.from(alertTriggers)
.where(and(eq(alertTriggers.organizationId, organizationId), eq(alertTriggers.elderId, elderId)))
.orderBy(desc(alertTriggers.createdAt))
.limit(8)
: Promise.resolve([]),
hasPermission(context, "family:read")
? database
.select()
.from(familyFeedback)
.where(and(eq(familyFeedback.organizationId, organizationId), eq(familyFeedback.elderId, elderId)))
.orderBy(desc(familyFeedback.createdAt))
.limit(8)
: Promise.resolve([]),
hasPermission(context, "incident:read")
? database
.select()
.from(systemIncidents)
.where(eq(systemIncidents.organizationId, organizationId))
.orderBy(desc(systemIncidents.createdAt))
.limit(6)
: Promise.resolve([]),
]);
if (admissionRows.length > 0) {
if (hasPermission(context, "admission:read")) {
dataScopes.push("admission");
}
if (hasPermission(context, "facility:read")) {
dataScopes.push("facility");
}
appendSection(
sections,
"入住与床位",
admissionRows.map((row) => {
const citationId = pushCitation(citations, {
sourceType: "resident_context",
sourceId: row.admission.id,
title: "入住记录",
excerpt: `${row.admission.status},床位 ${row.roomCode ?? ""}-${row.bedCode ?? ""}`,
});
return `[${citationId}] 状态:${row.admission.status};房间/床位:${row.roomCode ?? "未知"}/${row.bedCode ?? "未知"};入住:${toIsoString(row.admission.admittedAt)};退住:${toIsoString(row.admission.dischargedAt)};备注:${row.admission.notes}`;
}),
);
}
if (hasPermission(context, "health:read")) {
dataScopes.push("health");
const profile = healthProfileRows[0];
appendSection(sections, "健康档案", [
profile
? `[${pushCitation(citations, {
sourceType: "resident_context",
sourceId: profile.id,
title: "健康档案",
excerpt: profile.medicalHistory || profile.medicationNotes || profile.emergencyNotes || "健康档案",
})}] 过敏:${profile.allergyNotes};病史:${profile.medicalHistory};用药:${profile.medicationNotes};照护限制:${profile.careRestrictions};紧急备注:${profile.emergencyNotes}`
: "暂无健康档案",
...vitalRows.map((vital) => {
const citationId = pushCitation(citations, {
sourceType: "resident_context",
sourceId: vital.id,
title: "生命体征",
excerpt: `${toIsoString(vital.recordedAt)} 心率 ${vital.heartRate ?? "-"} 血压 ${vital.systolicBp ?? "-"}/${vital.diastolicBp ?? "-"}`,
});
return `[${citationId}] ${toIsoString(vital.recordedAt)};来源:${vital.source};血压:${vital.systolicBp ?? "-"}/${vital.diastolicBp ?? "-"};心率:${vital.heartRate ?? "-"};体温:${vital.temperatureTenths ? vital.temperatureTenths / 10 : "-"};血氧:${vital.spo2 ?? "-"};备注:${vital.notes}`;
}),
...conditionRows.map((condition) => {
const citationId = pushCitation(citations, {
sourceType: "resident_context",
sourceId: condition.id,
title: "慢病记录",
excerpt: `${condition.name} ${condition.status}`,
});
return `[${citationId}] 慢病:${condition.name};状态:${condition.status};治疗:${condition.treatmentNotes};随访:${condition.followUpNotes}`;
}),
...reviewRows.map((review) => {
const citationId = pushCitation(citations, {
sourceType: "resident_context",
sourceId: review.id,
title: "健康异常复核",
excerpt: `${review.severity} ${review.title}`,
});
return `[${citationId}] 异常:${review.title};严重度:${review.severity};状态:${review.status};说明:${review.description};处理:${review.resolutionNotes}`;
}),
]);
}
if (hasPermission(context, "care:read")) {
dataScopes.push("care");
appendSection(
sections,
"护理任务",
careTaskRows.map((task) => {
const citationId = pushCitation(citations, {
sourceType: "resident_context",
sourceId: task.id,
title: "护理任务",
excerpt: `${task.priority} ${task.title}`,
});
return `[${citationId}] ${task.title};类型:${task.careType};优先级:${task.priority};状态:${task.status};计划:${toIsoString(task.scheduledAt)};执行备注:${task.executionNotes}`;
}),
);
}
if (hasPermission(context, "alert:read")) {
dataScopes.push("alert");
appendSection(
sections,
"预警触发",
alertRows.map((alert) => {
const citationId = pushCitation(citations, {
sourceType: "resident_context",
sourceId: alert.id,
title: "预警触发",
excerpt: `${alert.title} ${alert.status}`,
});
return `[${citationId}] ${alert.title};状态:${alert.status};来源:${alert.source};说明:${alert.description};处理:${alert.handlingNotes}`;
}),
);
}
if (hasPermission(context, "family:read")) {
dataScopes.push("family");
appendSection(
sections,
"家属反馈",
feedbackRows.map((feedback) => {
const citationId = pushCitation(citations, {
sourceType: "resident_context",
sourceId: feedback.id,
title: "家属反馈",
excerpt: `${feedback.feedbackType} ${feedback.status}`,
});
return `[${citationId}] 类型:${feedback.feedbackType};状态:${feedback.status};内容:${feedback.content};回复:${feedback.responseNotes}`;
}),
);
}
if (hasPermission(context, "incident:read")) {
dataScopes.push("incident");
appendSection(
sections,
"近期机构事件",
incidentRows.map((incident) => {
const citationId = pushCitation(citations, {
sourceType: "resident_context",
sourceId: incident.id,
title: "系统/应急事件",
excerpt: `${incident.severity} ${incident.title}`,
});
return `[${citationId}] ${incident.title};严重度:${incident.severity};状态:${incident.status};来源:${incident.source};说明:${incident.description}`;
}),
);
}
const uniqueScopes = [...new Set(dataScopes)];
return {
success: true,
context: {
organizationId,
elderId,
elderName: elder.name,
dataScopes: uniqueScopes,
sections,
citations,
promptContext: sections.join("\n\n"),
},
};
}