Files
vueapp/api/learning-assistant.ts
T
2026-06-24 11:11:11 +08:00

263 lines
7.0 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.
import { FASTAPI_BASE_URL, authHeaders, readError, type ApiEnvelope } from './session'
export type LearningAssistantSession = {
assistant_session_id: number | string
title?: string
}
type WorkflowRunResponse = {
workflow_run_id?: string
task_id?: string
data?: {
status?: string
outputs?: unknown
error?: string
[key: string]: unknown
}
[key: string]: unknown
}
type ChatStreamPayload = {
question: string
top_k?: number
score_threshold?: number
}
type StreamCallbacks = {
onDelta: (delta: string) => void
onDone?: (meta: Record<string, unknown>) => void
}
const CLINICAL_BOOK_WORKFLOW_URL = import.meta.env.DEV
? '/dify/v1/workflows/run'
: 'http://8.160.178.88:8088/v1/workflows/run'
const CLINICAL_BOOK_WORKFLOW_API_KEY = 'app-9BapxGXgF2qQi9HHkq4R95Y7'
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 runClinicalThinkingBooksWorkflow(input: string, signal?: AbortSignal) {
const response = await fetch(CLINICAL_BOOK_WORKFLOW_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${CLINICAL_BOOK_WORKFLOW_API_KEY}`
},
body: JSON.stringify({
inputs: {
input
},
response_mode: 'blocking',
user: readWorkflowUserId()
}),
signal
})
if (!response.ok) {
throw new Error(await readWorkflowError(response))
}
const result = (await response.json()) as WorkflowRunResponse
if (result.data?.status && result.data.status !== 'succeeded') {
throw new Error(result.data.error || '临床思维训练书籍生成失败')
}
const output = readWorkflowOutput(result)
if (!output) {
throw new Error('临床思维训练书籍内容为空,请稍后重试')
}
return output
}
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 readWorkflowUserId() {
const storageKey = 'clinical-thinking-workflow-user-id'
const stored = uni.getStorageSync(storageKey)
if (typeof stored === 'string' && stored.trim()) return stored
const generated = `clinical-thinking-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
uni.setStorageSync(storageKey, generated)
return generated
}
async function readWorkflowError(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
}
function readWorkflowOutput(result: WorkflowRunResponse) {
const data = result.data
if (data && typeof data === 'object') {
const output = readOutputText(data.outputs)
|| readOutputText(data.output)
|| readOutputText(data.answer)
|| readOutputText(data.text)
|| readOutputText(data.result)
if (output) return output
}
return readOutputText(result.output)
|| readOutputText(result.answer)
|| readOutputText(result.text)
|| readOutputText(result.result)
}
function readOutputText(value: unknown): string {
if (typeof value === 'string') return value.trim()
if (Array.isArray(value)) {
return value.map(readOutputText).filter(Boolean).join('\n')
}
if (!value || typeof value !== 'object') return ''
const payload = value as Record<string, unknown>
const preferredKeys = ['answer', 'text', 'result', 'output', 'content', 'markdown', 'data']
for (const key of preferredKeys) {
const text = readOutputText(payload[key])
if (text) return text
}
const entries = Object.entries(payload)
.map(([key, entry]) => {
const text = readOutputText(entry)
return text ? `${key}${text}` : ''
})
.filter(Boolean)
return entries.join('\n')
}
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 ''
}