Files
vueapp/api/session.ts
T
2026-06-05 15:27:29 +08:00

206 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('练习提示未正常结束,请重试')
}
}