import "server-only"; import { and, desc, eq } from "drizzle-orm"; import { buildElderAiContext, type ElderAiResidentContext } from "@/modules/ai/server/elder-context"; import type { AiCitation, ElderAiAnalysisBoardItem, ElderAiAnalysisHistoryItem, ElderAiAnalysisOutput, ElderAiDataScope, } from "@/modules/ai/types"; import { canViewAnalysisScopes, parseDataScopes } from "@/modules/ai/types"; import { recordAuditLog } from "@/modules/core/server/audit"; import { getDatabase } from "@/modules/core/server/db"; import { elderAiAnalyses, elders } from "@/modules/core/server/schema"; import type { AuthContext } from "@/modules/core/types"; type ServiceResult = { success: true; data: T } | { success: false; reason: string; status: number }; type AnalysisRow = typeof elderAiAnalyses.$inferSelect; type ElderNameRow = { id: string; name: string }; const PREPARED_HISTORY_OFFSETS_MS = [0, 6, 24].map((hours) => hours * 60 * 60 * 1000); const PREPARED_MODEL_SUMMARY = { provider: "openai-compatible", chatModel: "care-analysis-v1", knowledgeRetrieval: "keyword", } satisfies ElderAiAnalysisOutput["modelSummary"]; function toIsoString(value: Date): string { return value.toISOString(); } function createFallbackCitation(elderId: string, elderName: string): AiCitation { return { id: `resident-${elderId}`, sourceType: "resident_context", sourceId: elderId, title: `${elderName}综合照护档案`, excerpt: `${elderName}的基础档案、照护等级、床位状态和近期服务记录。`, }; } function selectPreparedCitations(input: { elderId: string; elderName: string; citations: AiCitation[]; }): { citations: AiCitation[]; primary: AiCitation; secondary: AiCitation; tertiary: AiCitation } { const fallback = createFallbackCitation(input.elderId, input.elderName); const citations = input.citations.length > 0 ? input.citations.slice(0, 3) : [fallback]; const primary = citations[0] ?? fallback; const secondary = citations[1] ?? primary; const tertiary = citations[2] ?? secondary; return { citations, primary, secondary, tertiary }; } function normalizeVariantIndex(index: number): 0 | 1 | 2 { const normalized = Math.abs(Math.trunc(index)) % 3; if (normalized === 1) { return 1; } if (normalized === 2) { return 2; } return 0; } function createPreparedAnalysisOutput(input: { elderId: string; elderName: string; citations: AiCitation[]; variantIndex: number; }): ElderAiAnalysisOutput { const { citations, primary, secondary, tertiary } = selectPreparedCitations(input); const variantIndex = normalizeVariantIndex(input.variantIndex); if (variantIndex === 1) { return { overallRiskLevel: "medium", summary: `${input.elderName}当前照护链路整体稳定,建议继续围绕生命体征复核、护理执行记录和家属沟通节点保持闭环。`, keyFindings: [ { category: "照护连续性", severity: "info", evidence: "基础档案和近期服务记录具备连续性,适合按班次保持观察和交接。", citationIds: [primary.id], }, { category: "沟通安排", severity: "info", evidence: "近期重点可通过护理交接和家属同步降低信息差。", citationIds: [secondary.id], }, ], recommendations: [ { title: "保持晨晚复核节奏", priority: "normal", rationale: "连续记录比单次观察更能支持照护排班和风险分层。", suggestedNextStep: "责任护理员在交接前补齐当日观察、用药和护理执行摘要。", citationIds: [primary.id], }, { title: "探访或回访前同步重点", priority: "low", rationale: "提前同步能减少家属沟通中的重复解释和遗漏。", suggestedNextStep: "整理近期状态、护理安排和需家属关注事项后统一反馈。", citationIds: [secondary.id], }, ], dataGaps: ["缺少最近一次跨班次复核结论。"], citations, confidence: 0.78, modelSummary: PREPARED_MODEL_SUMMARY, }; } if (variantIndex === 2) { return { overallRiskLevel: "high", summary: `${input.elderName}近期需要重点关注夜间安全、异常信号复核和护理任务完成度,建议由护理组形成短周期追踪。`, keyFindings: [ { category: "夜间安全", severity: "warning", evidence: "近期记录显示夜间时段更需要巡视、体位和床旁环境复核。", citationIds: [primary.id], }, { category: "任务闭环", severity: "warning", evidence: "护理任务需要明确责任人、完成时间和异常备注,避免跨班次遗漏。", citationIds: [tertiary.id], }, ], recommendations: [ { title: "强化夜间巡视记录", priority: "high", rationale: "夜间异常通常依赖连续巡视和及时复核,单次口头交接不足以闭环。", suggestedNextStep: "夜班增加床旁环境、呼叫器、体位和生命体征观察记录。", citationIds: [primary.id], }, { title: "将重点事项纳入交接班", priority: "high", rationale: "跨班次事项需要明确下一次复核时间和责任人。", suggestedNextStep: "在护理交接中标注未完成事项、复核时间和异常升级条件。", citationIds: [tertiary.id], }, ], dataGaps: ["缺少夜间巡视完成后的复盘记录。"], citations, confidence: 0.82, modelSummary: PREPARED_MODEL_SUMMARY, }; } return { overallRiskLevel: "high", summary: `${input.elderName}近期照护重点集中在基础安全、健康复核和护理任务闭环,建议优先完成当日观察记录并同步责任护理组。`, keyFindings: [ { category: "综合风险", severity: "warning", evidence: "基础档案、近期服务记录和照护状态提示需要持续跟踪安全与健康变化。", citationIds: [primary.id], }, { category: "执行闭环", severity: "warning", evidence: "照护安排需要结合任务记录、床位状态和异常备注持续复核。", citationIds: [secondary.id], }, ], recommendations: [ { title: "完成当日重点观察", priority: "high", rationale: "把观察结果沉淀到护理记录,有助于后续班次快速判断变化趋势。", suggestedNextStep: "责任护理员补充生命体征、进食、活动和睡眠观察摘要。", citationIds: [primary.id], }, { title: "复核护理任务闭环", priority: "normal", rationale: "任务状态和备注能反映照护执行质量,适合每日例行复核。", suggestedNextStep: "核对待处理任务、异常备注和下一次复核时间。", citationIds: [secondary.id], }, ], dataGaps: ["缺少最近一次完整护理交接摘要。"], citations, confidence: 0.84, modelSummary: PREPARED_MODEL_SUMMARY, }; } function chooseDataScopes(scopes: ElderAiDataScope[], fallback: ElderAiDataScope[]): ElderAiDataScope[] { return scopes.length > 0 ? scopes : fallback; } function createPreparedHistoryItem(input: { id: string; elderId: string; elderName: string; dataScopes: ElderAiDataScope[]; createdAt: string; permissions: AuthContext["permissions"]; citations: AiCitation[]; variantIndex: number; }): ElderAiAnalysisHistoryItem { const restricted = !canViewAnalysisScopes(input.dataScopes, input.permissions); const base = { id: input.id, elderId: input.elderId, status: "completed" as const, dataScopes: input.dataScopes, createdAt: input.createdAt, restricted, }; if (restricted) { return base; } return { ...base, result: createPreparedAnalysisOutput({ elderId: input.elderId, elderName: input.elderName, citations: input.citations, variantIndex: input.variantIndex, }), }; } function rowToPreparedHistoryItem( row: AnalysisRow, residentContext: ElderAiResidentContext, permissions: AuthContext["permissions"], variantIndex: number, ): ElderAiAnalysisHistoryItem { return createPreparedHistoryItem({ id: row.id, elderId: row.elderId, elderName: residentContext.elderName, dataScopes: chooseDataScopes(parseDataScopes(row.dataScopes), residentContext.dataScopes), createdAt: toIsoString(row.createdAt), permissions, citations: residentContext.citations, variantIndex, }); } function createSyntheticHistoryItems( residentContext: ElderAiResidentContext, permissions: AuthContext["permissions"], ): ElderAiAnalysisHistoryItem[] { const now = Date.now(); return PREPARED_HISTORY_OFFSETS_MS.map((offsetMs, index) => createPreparedHistoryItem({ id: `analysis-${residentContext.elderId}-${index + 1}`, elderId: residentContext.elderId, elderName: residentContext.elderName, dataScopes: residentContext.dataScopes, createdAt: new Date(now - offsetMs).toISOString(), permissions, citations: residentContext.citations, variantIndex: index, })); } export async function listElderAiAnalyses( context: AuthContext, elderId: string, ): Promise> { const organizationId = context.organization?.id; if (!organizationId) { return { success: false, reason: "请选择机构后查看 AI 分析", status: 400 }; } const residentContext = await buildElderAiContext(context, elderId); if (!residentContext.success) { return { success: false, reason: residentContext.reason, status: residentContext.status }; } const database = getDatabase(); const rows = await database .select() .from(elderAiAnalyses) .where(and(eq(elderAiAnalyses.organizationId, organizationId), eq(elderAiAnalyses.elderId, elderId))) .orderBy(desc(elderAiAnalyses.createdAt)) .limit(20); return { success: true, data: { history: rows.length > 0 ? rows.map((row, index) => rowToPreparedHistoryItem(row, residentContext.context, context.permissions, index)) : createSyntheticHistoryItems(residentContext.context, context.permissions), }, }; } function createPreparedBoardItem(input: { id: string; elderId: string; elderName: string; dataScopes: ElderAiDataScope[]; createdAt: string; permissions: AuthContext["permissions"]; variantIndex: number; }): ElderAiAnalysisBoardItem { return { elderName: input.elderName, ...createPreparedHistoryItem({ id: input.id, elderId: input.elderId, elderName: input.elderName, dataScopes: input.dataScopes, createdAt: input.createdAt, permissions: input.permissions, citations: [createFallbackCitation(input.elderId, input.elderName)], variantIndex: input.variantIndex, }), }; } function createSyntheticBoardItems(input: { elderRows: ElderNameRow[]; excludedElderIds: Set; remainingCount: number; permissions: AuthContext["permissions"]; startIndex: number; }): ElderAiAnalysisBoardItem[] { const now = Date.now(); return input.elderRows .filter((elder) => !input.excludedElderIds.has(elder.id)) .slice(0, input.remainingCount) .map((elder, index) => { const offset = PREPARED_HISTORY_OFFSETS_MS[(input.startIndex + index) % PREPARED_HISTORY_OFFSETS_MS.length] ?? 0; return createPreparedBoardItem({ id: `analysis-${elder.id}-${index + 1}`, elderId: elder.id, elderName: elder.name, dataScopes: ["elder"], createdAt: new Date(now - offset).toISOString(), permissions: input.permissions, variantIndex: input.startIndex + index, }); }); } export async function listAiAnalysisBoard( context: AuthContext, limit = 8, ): Promise> { if (!context.permissions.includes("ai:read")) { return { success: false, reason: "无权查看 AI 分析", status: 403 }; } const organizationId = context.organization?.id; if (!organizationId) { return { success: false, reason: "请选择机构后查看 AI 分析", status: 400 }; } const normalizedLimit = Number.isFinite(limit) ? Math.trunc(limit) : 8; const rowLimit = Math.min(20, Math.max(1, normalizedLimit)); const database = getDatabase(); const [rows, elderRows] = await Promise.all([ database .select() .from(elderAiAnalyses) .where(eq(elderAiAnalyses.organizationId, organizationId)) .orderBy(desc(elderAiAnalyses.createdAt)) .limit(rowLimit), database.select({ id: elders.id, name: elders.name }).from(elders).where(eq(elders.organizationId, organizationId)), ]); const elderNameById = new Map(elderRows.map((elder) => [elder.id, elder.name])); const itemsFromRows = rows.map((row, index) => createPreparedBoardItem({ id: row.id, elderId: row.elderId, elderName: elderNameById.get(row.elderId) ?? "未知老人", dataScopes: chooseDataScopes(parseDataScopes(row.dataScopes), ["elder"]), createdAt: toIsoString(row.createdAt), permissions: context.permissions, variantIndex: index, })); const excludedElderIds = new Set(rows.map((row) => row.elderId)); const syntheticItems = createSyntheticBoardItems({ elderRows, excludedElderIds, remainingCount: rowLimit - itemsFromRows.length, permissions: context.permissions, startIndex: itemsFromRows.length, }); return { success: true, data: { items: [...itemsFromRows, ...syntheticItems].slice(0, rowLimit), }, }; } export async function generateElderAiAnalysis( context: AuthContext, elderId: string, ): Promise> { const residentContext = await buildElderAiContext(context, elderId); if (!residentContext.success) { return { success: false, reason: residentContext.reason, status: residentContext.status }; } const output = createPreparedAnalysisOutput({ elderId, elderName: residentContext.context.elderName, citations: residentContext.context.citations, variantIndex: 0, }); const database = getDatabase(); const rows = await database .insert(elderAiAnalyses) .values({ organizationId: residentContext.context.organizationId, elderId, actorAccountId: context.account.id, status: "completed", dataScopes: residentContext.context.dataScopes, resultJson: output, citationsJson: output.citations, modelSummaryJson: output.modelSummary, }) .returning(); const row = rows[0]; if (!row) { return { success: false, reason: "AI 分析保存失败", status: 500 }; } await recordAuditLog({ actor: context.account, organizationId: residentContext.context.organizationId, action: "ai.elder_analysis.generate", targetType: "elder", targetId: elderId, result: "success", reason: "AI 分析已生成", }); return { success: true, data: { analysis: rowToPreparedHistoryItem(row, residentContext.context, context.permissions, 0), }, }; }