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) => 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' const CLINICAL_BOOK_WORKFLOW_TRIGGER = '临床思维训练书籍' 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> 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(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: CLINICAL_BOOK_WORKFLOW_TRIGGER }, 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) { 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 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 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).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 : {}) } } function readDelta(data: unknown) { if (typeof data === 'string') return data if (!data || typeof data !== 'object') return '' const payload = data as Record 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 const nestedDelta = nested.delta || nested.content || nested.answer || nested.text if (typeof nestedDelta === 'string') return nestedDelta } return '' }