- 新增 analysisSse.ts SSE 流式分析 API 封装(ReadableStream 解析) - 化验报告页操作列添加 AI 解读按钮(SSE 实时流式输出) - 分析结果展示在 Table 下方的 Card 中
97 lines
2.6 KiB
TypeScript
97 lines
2.6 KiB
TypeScript
export type AnalysisType = 'lab-report' | 'trends' | 'checkup-plan' | 'report-summary';
|
|
|
|
interface AnalyzeBody {
|
|
report_id?: string;
|
|
patient_id?: string;
|
|
metrics?: string[];
|
|
}
|
|
|
|
const ENDPOINT_MAP: Record<AnalysisType, string> = {
|
|
'lab-report': '/ai/analyze/lab-report',
|
|
'trends': '/ai/analyze/trends',
|
|
'checkup-plan': '/ai/analyze/checkup-plan',
|
|
'report-summary': '/ai/analyze/report-summary',
|
|
};
|
|
|
|
export interface SseCallbacks {
|
|
onChunk: (content: string, index: number) => void;
|
|
onError: (message: string) => void;
|
|
onDone: (analysisId: string) => void;
|
|
}
|
|
|
|
export async function startAnalysis(
|
|
type: AnalysisType,
|
|
body: AnalyzeBody,
|
|
callbacks: SseCallbacks,
|
|
): Promise<AbortController> {
|
|
const controller = new AbortController();
|
|
const endpoint = ENDPOINT_MAP[type];
|
|
|
|
const token = localStorage.getItem('hms-token');
|
|
const resp = await fetch(`/api/v1${endpoint}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify(body),
|
|
signal: controller.signal,
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
const err = await resp.json().catch(() => ({ message: '分析请求失败' }));
|
|
callbacks.onError(err?.message || `HTTP ${resp.status}`);
|
|
return controller;
|
|
}
|
|
|
|
const reader = resp.body?.getReader();
|
|
if (!reader) {
|
|
callbacks.onError('无法读取响应流');
|
|
return controller;
|
|
}
|
|
|
|
const decoder = new TextDecoder();
|
|
let chunkIndex = 0;
|
|
let buffer = '';
|
|
|
|
(async () => {
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const lines = buffer.split('\n');
|
|
buffer = lines.pop() || '';
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith('data: ')) {
|
|
const data = line.slice(6);
|
|
if (data === '[DONE]') {
|
|
continue;
|
|
}
|
|
try {
|
|
const event = JSON.parse(data);
|
|
if (event.type === 'chunk' && event.content) {
|
|
callbacks.onChunk(event.content, chunkIndex++);
|
|
} else if (event.type === 'done' && event.analysis_id) {
|
|
callbacks.onDone(event.analysis_id);
|
|
} else if (event.type === 'error') {
|
|
callbacks.onError(event.message || '分析出错');
|
|
}
|
|
} catch {
|
|
// 非 JSON 行,跳过
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
if (!controller.signal.aborted) {
|
|
callbacks.onError(err instanceof Error ? err.message : '连接中断');
|
|
}
|
|
}
|
|
})();
|
|
|
|
return controller;
|
|
}
|