333 lines
9.8 KiB
TypeScript
333 lines
9.8 KiB
TypeScript
import type { Permission } from "@/modules/core/types";
|
|
|
|
export const AI_KNOWLEDGE_SCOPES = ["platform", "organization"] as const;
|
|
export type AiKnowledgeScope = (typeof AI_KNOWLEDGE_SCOPES)[number];
|
|
|
|
export const AI_KNOWLEDGE_STATUSES = ["enabled", "disabled"] as const;
|
|
export type AiKnowledgeStatus = (typeof AI_KNOWLEDGE_STATUSES)[number];
|
|
|
|
export const ELDER_AI_ANALYSIS_STATUSES = ["completed", "failed"] as const;
|
|
export type ElderAiAnalysisStatus = (typeof ELDER_AI_ANALYSIS_STATUSES)[number];
|
|
|
|
export const ELDER_AI_DATA_SCOPES = [
|
|
"elder",
|
|
"health",
|
|
"care",
|
|
"family",
|
|
"admission",
|
|
"facility",
|
|
"alert",
|
|
"incident",
|
|
"knowledge",
|
|
] as const;
|
|
export type ElderAiDataScope = (typeof ELDER_AI_DATA_SCOPES)[number];
|
|
|
|
export const ELDER_AI_RISK_LEVELS = ["low", "medium", "high", "critical", "unknown"] as const;
|
|
export type ElderAiRiskLevel = (typeof ELDER_AI_RISK_LEVELS)[number];
|
|
|
|
export const ELDER_AI_SEVERITIES = ["info", "warning", "critical"] as const;
|
|
export type ElderAiSeverity = (typeof ELDER_AI_SEVERITIES)[number];
|
|
|
|
export const ELDER_AI_RECOMMENDATION_PRIORITIES = ["low", "normal", "high", "urgent"] as const;
|
|
export type ElderAiRecommendationPriority = (typeof ELDER_AI_RECOMMENDATION_PRIORITIES)[number];
|
|
|
|
export const AI_ERROR_CATEGORIES = [
|
|
"missing_config",
|
|
"provider_error",
|
|
"timeout",
|
|
"schema_validation_failed",
|
|
"retrieval_failed",
|
|
"unknown",
|
|
] as const;
|
|
export type AiErrorCategory = (typeof AI_ERROR_CATEGORIES)[number];
|
|
|
|
export type AiCitation = {
|
|
id: string;
|
|
sourceType: "resident_context" | "knowledge";
|
|
sourceId: string;
|
|
title: string;
|
|
excerpt: string;
|
|
};
|
|
|
|
export type ElderAiFinding = {
|
|
category: string;
|
|
severity: ElderAiSeverity;
|
|
evidence: string;
|
|
citationIds: string[];
|
|
};
|
|
|
|
export type ElderAiRecommendation = {
|
|
title: string;
|
|
priority: ElderAiRecommendationPriority;
|
|
rationale: string;
|
|
suggestedNextStep: string;
|
|
citationIds: string[];
|
|
};
|
|
|
|
export type ElderAiModelSummary = {
|
|
provider: "openai-compatible";
|
|
chatModel: string;
|
|
embeddingModel: string;
|
|
};
|
|
|
|
export type ElderAiAnalysisOutput = {
|
|
overallRiskLevel: ElderAiRiskLevel;
|
|
summary: string;
|
|
keyFindings: ElderAiFinding[];
|
|
recommendations: ElderAiRecommendation[];
|
|
dataGaps: string[];
|
|
citations: AiCitation[];
|
|
confidence: number;
|
|
modelSummary: ElderAiModelSummary;
|
|
};
|
|
|
|
export type ElderAiAnalysisHistoryItem = {
|
|
id: string;
|
|
elderId: string;
|
|
status: ElderAiAnalysisStatus;
|
|
dataScopes: ElderAiDataScope[];
|
|
createdAt: string;
|
|
restricted: boolean;
|
|
result?: ElderAiAnalysisOutput;
|
|
errorCategory?: AiErrorCategory;
|
|
errorReason?: string;
|
|
};
|
|
|
|
export type KnowledgeEntryInput = {
|
|
scope: AiKnowledgeScope;
|
|
organizationId?: string;
|
|
title: string;
|
|
category: string;
|
|
tags: string;
|
|
body: string;
|
|
status: AiKnowledgeStatus;
|
|
};
|
|
|
|
export type KnowledgeEntry = KnowledgeEntryInput & {
|
|
id: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type KnowledgeRetrievalResult = {
|
|
entryId: string;
|
|
chunkId: string;
|
|
title: string;
|
|
category: string;
|
|
content: string;
|
|
score: number;
|
|
};
|
|
|
|
const DATA_SCOPE_PERMISSION_MAP: Record<Exclude<ElderAiDataScope, "elder" | "knowledge">, Permission> = {
|
|
health: "health:read",
|
|
care: "care:read",
|
|
family: "family:read",
|
|
admission: "admission:read",
|
|
facility: "facility:read",
|
|
alert: "alert:read",
|
|
incident: "incident:read",
|
|
};
|
|
|
|
export function isElderAiDataScope(value: string): value is ElderAiDataScope {
|
|
return (ELDER_AI_DATA_SCOPES as readonly string[]).includes(value);
|
|
}
|
|
|
|
export function getPermissionForDataScope(scope: ElderAiDataScope): Permission {
|
|
if (scope === "elder") {
|
|
return "elder:read";
|
|
}
|
|
if (scope === "knowledge") {
|
|
return "knowledge:read";
|
|
}
|
|
return DATA_SCOPE_PERMISSION_MAP[scope];
|
|
}
|
|
|
|
export function canViewAnalysisScopes(scopes: readonly ElderAiDataScope[], permissions: readonly Permission[]): boolean {
|
|
return scopes.every((scope) => permissions.includes(getPermissionForDataScope(scope)));
|
|
}
|
|
|
|
export function parseDataScopes(value: unknown): ElderAiDataScope[] {
|
|
if (!Array.isArray(value)) {
|
|
return [];
|
|
}
|
|
return value.filter((item): item is ElderAiDataScope => typeof item === "string" && isElderAiDataScope(item));
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function readStringArray(value: unknown): string[] | null {
|
|
if (!Array.isArray(value)) {
|
|
return null;
|
|
}
|
|
const items = value.filter((item): item is string => typeof item === "string");
|
|
return items.length === value.length ? items : null;
|
|
}
|
|
|
|
function validateCitation(value: unknown): AiCitation | null {
|
|
if (!isRecord(value)) {
|
|
return null;
|
|
}
|
|
const sourceType = value.sourceType;
|
|
if (sourceType !== "resident_context" && sourceType !== "knowledge") {
|
|
return null;
|
|
}
|
|
if (
|
|
typeof value.id !== "string" ||
|
|
typeof value.sourceId !== "string" ||
|
|
typeof value.title !== "string" ||
|
|
typeof value.excerpt !== "string"
|
|
) {
|
|
return null;
|
|
}
|
|
return {
|
|
id: value.id,
|
|
sourceType,
|
|
sourceId: value.sourceId,
|
|
title: value.title,
|
|
excerpt: value.excerpt,
|
|
};
|
|
}
|
|
|
|
function validateFinding(value: unknown): ElderAiFinding | null {
|
|
if (!isRecord(value)) {
|
|
return null;
|
|
}
|
|
const citationIds = readStringArray(value.citationIds);
|
|
if (
|
|
typeof value.category !== "string" ||
|
|
typeof value.evidence !== "string" ||
|
|
!ELDER_AI_SEVERITIES.includes(value.severity as ElderAiSeverity) ||
|
|
!citationIds
|
|
) {
|
|
return null;
|
|
}
|
|
return {
|
|
category: value.category,
|
|
severity: value.severity as ElderAiSeverity,
|
|
evidence: value.evidence,
|
|
citationIds,
|
|
};
|
|
}
|
|
|
|
function validateRecommendation(value: unknown): ElderAiRecommendation | null {
|
|
if (!isRecord(value)) {
|
|
return null;
|
|
}
|
|
const citationIds = readStringArray(value.citationIds);
|
|
if (
|
|
typeof value.title !== "string" ||
|
|
typeof value.rationale !== "string" ||
|
|
typeof value.suggestedNextStep !== "string" ||
|
|
!ELDER_AI_RECOMMENDATION_PRIORITIES.includes(value.priority as ElderAiRecommendationPriority) ||
|
|
!citationIds
|
|
) {
|
|
return null;
|
|
}
|
|
return {
|
|
title: value.title,
|
|
priority: value.priority as ElderAiRecommendationPriority,
|
|
rationale: value.rationale,
|
|
suggestedNextStep: value.suggestedNextStep,
|
|
citationIds,
|
|
};
|
|
}
|
|
|
|
function validateModelSummary(value: unknown): ElderAiModelSummary | null {
|
|
if (!isRecord(value)) {
|
|
return null;
|
|
}
|
|
if (
|
|
value.provider !== "openai-compatible" ||
|
|
typeof value.chatModel !== "string" ||
|
|
typeof value.embeddingModel !== "string"
|
|
) {
|
|
return null;
|
|
}
|
|
return {
|
|
provider: "openai-compatible",
|
|
chatModel: value.chatModel,
|
|
embeddingModel: value.embeddingModel,
|
|
};
|
|
}
|
|
|
|
export function validateElderAiAnalysisOutput(value: unknown): ElderAiAnalysisOutput | null {
|
|
if (!isRecord(value)) {
|
|
return null;
|
|
}
|
|
const confidenceValue = typeof value.confidence === "string" ? Number(value.confidence) : value.confidence;
|
|
const keyFindings = Array.isArray(value.keyFindings) ? value.keyFindings.map(validateFinding) : null;
|
|
const recommendations = Array.isArray(value.recommendations) ? value.recommendations.map(validateRecommendation) : null;
|
|
const citations = Array.isArray(value.citations) ? value.citations.map(validateCitation) : null;
|
|
const dataGaps = readStringArray(value.dataGaps);
|
|
const modelSummary = validateModelSummary(value.modelSummary);
|
|
|
|
if (
|
|
!ELDER_AI_RISK_LEVELS.includes(value.overallRiskLevel as ElderAiRiskLevel) ||
|
|
typeof value.summary !== "string" ||
|
|
typeof confidenceValue !== "number" ||
|
|
!Number.isFinite(confidenceValue) ||
|
|
!keyFindings ||
|
|
keyFindings.includes(null) ||
|
|
!recommendations ||
|
|
recommendations.includes(null) ||
|
|
!dataGaps ||
|
|
!citations ||
|
|
citations.includes(null) ||
|
|
!modelSummary
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
overallRiskLevel: value.overallRiskLevel as ElderAiRiskLevel,
|
|
summary: value.summary,
|
|
keyFindings: keyFindings.filter((item): item is ElderAiFinding => Boolean(item)),
|
|
recommendations: recommendations.filter((item): item is ElderAiRecommendation => Boolean(item)),
|
|
dataGaps,
|
|
citations: citations.filter((item): item is AiCitation => Boolean(item)),
|
|
confidence: Math.max(0, Math.min(1, confidenceValue)),
|
|
modelSummary,
|
|
};
|
|
}
|
|
|
|
export function validateKnowledgeEntryInput(value: unknown): { success: true; input: KnowledgeEntryInput } | { success: false; reason: string } {
|
|
if (!value || typeof value !== "object") {
|
|
return { success: false, reason: "请求数据格式无效" };
|
|
}
|
|
const record = value as Record<string, unknown>;
|
|
const scope = typeof record.scope === "string" ? record.scope : "";
|
|
const title = typeof record.title === "string" ? record.title.trim() : "";
|
|
const category = typeof record.category === "string" ? record.category.trim() : "";
|
|
const tags = typeof record.tags === "string" ? record.tags.trim() : "";
|
|
const body = typeof record.body === "string" ? record.body.trim() : "";
|
|
const status = typeof record.status === "string" ? record.status : "enabled";
|
|
const organizationId = typeof record.organizationId === "string" && record.organizationId.trim() ? record.organizationId.trim() : undefined;
|
|
|
|
if (!AI_KNOWLEDGE_SCOPES.includes(scope as AiKnowledgeScope)) {
|
|
return { success: false, reason: "知识库范围无效" };
|
|
}
|
|
if (!AI_KNOWLEDGE_STATUSES.includes(status as AiKnowledgeStatus)) {
|
|
return { success: false, reason: "知识库状态无效" };
|
|
}
|
|
if (!title) {
|
|
return { success: false, reason: "知识标题不能为空" };
|
|
}
|
|
if (!body) {
|
|
return { success: false, reason: "知识内容不能为空" };
|
|
}
|
|
return {
|
|
success: true,
|
|
input: {
|
|
scope: scope as AiKnowledgeScope,
|
|
organizationId,
|
|
title,
|
|
category,
|
|
tags,
|
|
body,
|
|
status: status as AiKnowledgeStatus,
|
|
},
|
|
};
|
|
}
|