Files
vueapp/api/learning-assistant.ts
T

149 lines
3.8 KiB
TypeScript
Raw Normal View History

2026-06-11 17:48:46 +08:00
import { FASTAPI_BASE_URL, authHeaders, readError, type ApiEnvelope } from './session'
export type LearningAssistantSession = {
assistant_session_id: number | string
title?: string
}
type ChatStreamPayload = {
question: string
top_k?: number
score_threshold?: number
}
type StreamCallbacks = {
onDelta: (delta: string) => void
onDone?: (meta: Record<string, unknown>) => void
}
export async function createLearningAssistantSession(title?: string) {
const body = title?.trim()
? {
title: title.trim().slice(0, 100)
}
: {}
const response = await fetch(`${FASTAPI_BASE_URL}/learning-assistant/sessions`, {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify(body)
})
if (!response.ok) {
throw new Error(await readError(response))
}
const result = (await response.json()) as ApiEnvelope<Record<string, unknown>>
if (result.code !== 'OK' || !result.data) {
throw new Error(result.message || '新建会话失败')
}
const assistantSessionId = readAssistantSessionId(result.data)
if (!assistantSessionId) {
throw new Error('新建会话返回缺少 assistant_session_id')
}
return {
...result.data,
assistant_session_id: assistantSessionId
} as LearningAssistantSession
}
export async function streamLearningAssistantChat(
assistantSessionId: number | string,
payload: ChatStreamPayload,
callbacks: StreamCallbacks,
signal?: AbortSignal
) {
const response = await fetch(`${FASTAPI_BASE_URL}/learning-assistant/sessions/${assistantSessionId}/chat/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 = ''
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) {
handleStreamBlock(block, callbacks)
}
}
if (buffer.trim()) {
handleStreamBlock(buffer, callbacks)
}
}
function readAssistantSessionId(data: Record<string, unknown>) {
const value = data.assistant_session_id || data.session_id || data.id
if (typeof value === 'number' || typeof value === 'string') return value
return ''
}
function handleStreamBlock(block: string, callbacks: StreamCallbacks) {
const event = block.match(/^event:\s*(.+)$/m)?.[1] || ''
const rawLines = block
.split('\n')
.filter(line => line.startsWith('data:'))
.map(line => line.replace(/^data:\s?/, ''))
if (rawLines.length === 0) return
const rawData = rawLines.join('\n')
if (rawData === '[DONE]') {
callbacks.onDone?.({})
return
}
let data: unknown = rawData
try {
data = JSON.parse(rawData)
} catch {}
if (event === 'error') {
const message = typeof data === 'object' && data
? (data as Record<string, unknown>).message
: data
throw new Error(typeof message === 'string' ? message : 'AI 学习助手回复失败')
}
const delta = readDelta(data)
if (delta) callbacks.onDelta(delta)
if (event === 'done' || event === 'message_done') {
callbacks.onDone?.(typeof data === 'object' && data ? data as Record<string, unknown> : {})
}
}
function readDelta(data: unknown) {
if (typeof data === 'string') return data
if (!data || typeof data !== 'object') return ''
const payload = data as Record<string, unknown>
const delta = payload.delta || payload.content || payload.answer || payload.text
if (typeof delta === 'string') return delta
const nestedData = payload.data
if (nestedData && typeof nestedData === 'object') {
const nested = nestedData as Record<string, unknown>
const nestedDelta = nested.delta || nested.content || nested.answer || nested.text
if (typeof nestedDelta === 'string') return nestedDelta
}
return ''
}