export const FASTAPI_BASE_URL = '/fastapi/api/v1' export type TrainingType = 'case_analysis' | 'diagnosis_treatment' | 'consultation' export type TrainingMode = 'practice' | 'teaching' export type ScoreType = 'percentage' | 'five_point' export type PatientConfigPayload = { visit_environment: 'outpatient' | 'emergency' | 'ward' age_group: 'child' | 'youth' | 'middle_aged' | 'elderly' education_level: 'primary_or_below' | 'secondary' | 'higher' personality: 'calm' | 'anxious' | 'impatient' | 'cooperative' | 'suspicious' } export type CreateSessionPayload = { case_id: number training_type: TrainingType mode: TrainingMode score_type: ScoreType patient_config: PatientConfigPayload } export type SessionPatientConfig = { values: PatientConfigPayload labels: Record } export type TrainingSession = { session_id: number session_code: string status: string patient_opening: string patient_config: SessionPatientConfig } export type CompleteInquiryResult = { session_id: number status: string } export type StoredTrainingScenario = { session?: TrainingSession [key: string]: unknown } export type ApiEnvelope = { code: string message: string data: T } type StreamCallbacks = { onDelta: (delta: string) => void onDone?: (meta: Record) => void } type HintStreamPayload = { last_user_message: string scope: 'current_conversation' } function readAccessToken() { const token = uni.getStorageSync('clinical-thinking-access-token') if (typeof token !== 'string' || !token.trim()) { throw new Error('登录已过期,请重新登录') } return token } export function authHeaders(accept = 'application/json') { return { 'Content-Type': 'application/json', Accept: accept, Authorization: `Bearer ${readAccessToken()}`, 'X-Entry-Scene': 'vue_frontend' } } export async function readError(response: Response) { const text = await response.text().catch(() => '') if (!text) return `请求失败(${response.status})` try { const payload = JSON.parse(text) as Record const message = payload.message || payload.detail || payload.error if (typeof message === 'string' && message.trim()) return message } catch {} return text } export function readStoredTrainingScenario() { const value = uni.getStorageSync('clinical-thinking-scenario') if (value && typeof value === 'object') return value as StoredTrainingScenario return null } export function readActiveSessionId() { const sessionId = readStoredTrainingScenario()?.session?.session_id if (typeof sessionId === 'number' && Number.isInteger(sessionId) && sessionId > 0) { return sessionId } throw new Error('未找到当前会话,请先生成模拟场景') } export function updateStoredSessionStatus(status: string) { const scenario = readStoredTrainingScenario() if (!scenario?.session) return uni.setStorageSync('clinical-thinking-scenario', { ...scenario, session: { ...scenario.session, status } }) } export async function createTrainingSession(payload: CreateSessionPayload) { const response = await fetch(`${FASTAPI_BASE_URL}/sessions`, { method: 'POST', headers: authHeaders(), body: JSON.stringify(payload) }) if (!response.ok) { throw new Error(await readError(response)) } const result = (await response.json()) as ApiEnvelope if (result.code !== 'OK' || !result.data?.session_id) { throw new Error(result.message || '新建会话失败') } return result.data } export async function completeInquiry(sessionId: number) { const response = await fetch(`${FASTAPI_BASE_URL}/sessions/${sessionId}/complete-inquiry`, { method: 'POST', headers: authHeaders() }) if (!response.ok) { throw new Error(await readError(response)) } const result = (await response.json()) as ApiEnvelope if (result.code !== 'OK' || !result.data?.session_id) { throw new Error(result.message || '完成采集失败') } return result.data } export async function streamSessionChat( sessionId: number, message: string, callbacks: StreamCallbacks, signal?: AbortSignal ) { const response = await fetch(`${FASTAPI_BASE_URL}/sessions/${sessionId}/chat/stream`, { method: 'POST', headers: authHeaders('text/event-stream'), body: JSON.stringify({ message }), signal }) if (!response.ok || !response.body) { throw new Error(await readError(response)) } const reader = response.body.getReader() const decoder = new TextDecoder() let buffer = '' let completed = false while (true) { const { value, done } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const blocks = buffer.split('\n\n') buffer = blocks.pop() || '' for (const block of blocks) { const event = block.match(/^event:\s*(.+)$/m)?.[1] const rawData = block.match(/^data:\s*(.+)$/m)?.[1] if (!event || !rawData) continue const data = JSON.parse(rawData) as Record if (event === 'message_delta') { const delta = data.delta if (typeof delta === 'string') callbacks.onDelta(delta) } else if (event === 'message_done') { completed = true callbacks.onDone?.(data) } else if (event === 'error') { throw new Error(typeof data.message === 'string' ? data.message : 'AI 流式回复异常') } } } if (!completed) { throw new Error('AI 流式回复未正常结束,请重试') } } export async function streamSessionHint( sessionId: number, lastUserMessage: string, callbacks: StreamCallbacks, signal?: AbortSignal ) { const payload: HintStreamPayload = { last_user_message: lastUserMessage, scope: 'current_conversation' } const response = await fetch(`${FASTAPI_BASE_URL}/sessions/${sessionId}/hints/stream`, { method: 'POST', headers: authHeaders('text/event-stream'), body: JSON.stringify(payload), signal }) if (!response.ok || !response.body) { throw new Error(await readError(response)) } const reader = response.body.getReader() const decoder = new TextDecoder() let buffer = '' let completed = false while (true) { const { value, done } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const blocks = buffer.split('\n\n') buffer = blocks.pop() || '' for (const block of blocks) { const event = block.match(/^event:\s*(.+)$/m)?.[1] const rawData = block.match(/^data:\s*(.+)$/m)?.[1] if (!event || !rawData) continue const data = JSON.parse(rawData) as Record if (event === 'hint_delta') { const delta = data.delta if (typeof delta === 'string') callbacks.onDelta(delta) } else if (event === 'hint_done') { completed = true callbacks.onDone?.(data) } else if (event === 'error') { throw new Error(typeof data.message === 'string' ? data.message : '练习提示生成失败,请稍后重试') } } } if (!completed) { throw new Error('练习提示未正常结束,请重试') } }