feat: add AI knowledge analysis MVP
This commit is contained in:
331
modules/ai/server/elder-context.ts
Normal file
331
modules/ai/server/elder-context.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
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"),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user