206 lines
5.4 KiB
TypeScript
206 lines
5.4 KiB
TypeScript
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<keyof PatientConfigPayload, string>
|
||
}
|
||
|
||
export type TrainingSession = {
|
||
session_id: number
|
||
session_code: string
|
||
status: string
|
||
patient_opening: string
|
||
patient_config: SessionPatientConfig
|
||
}
|
||
|
||
type ApiEnvelope<T> = {
|
||
code: string
|
||
message: string
|
||
data: T
|
||
}
|
||
|
||
type StreamCallbacks = {
|
||
onDelta: (delta: string) => void
|
||
onDone?: (meta: Record<string, unknown>) => 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
|
||
}
|
||
|
||
function authHeaders(accept = 'application/json') {
|
||
return {
|
||
'Content-Type': 'application/json',
|
||
Accept: accept,
|
||
Authorization: `Bearer ${readAccessToken()}`,
|
||
'X-Entry-Scene': 'vue_frontend'
|
||
}
|
||
}
|
||
|
||
async function readError(response: Response) {
|
||
const text = await response.text().catch(() => '')
|
||
if (!text) return `请求失败(${response.status})`
|
||
try {
|
||
const payload = JSON.parse(text) as Record<string, unknown>
|
||
const message = payload.message || payload.detail || payload.error
|
||
if (typeof message === 'string' && message.trim()) return message
|
||
} catch {}
|
||
return text
|
||
}
|
||
|
||
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<TrainingSession>
|
||
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<string, unknown>
|
||
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<string, unknown>
|
||
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('练习提示未正常结束,请重试')
|
||
}
|
||
}
|