/** * AI 模块 API 封装 * SSE 流式对话 — 支持 intent / status / content / meta 多事件 */ const BASE_URL = 'https://your-api-domain.com/api/v1' // 本地调试: const BASE_URL = 'http://localhost:8000/api/v1' function getToken() { return uni.getStorageSync('access_token') || '' } function request(url, options = {}) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + url, method: options.method || 'GET', data: options.data, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getToken() }, success: (res) => { if (res.statusCode === 200) resolve(res.data) else if (res.statusCode === 401) { uni.navigateTo({ url: '/pages/login/index' }) reject(new Error('Unauthorized')) } else reject(new Error(res.data?.detail || 'HTTP ' + res.statusCode)) }, fail: (err) => reject(err) }) }) } /** * SSE 流式对话(支持多事件类型) * callbacks: { onIntent, onStatus, onToken, onMeta, onDone, onError } */ export function chatStream(data, callbacks) { const { onToken, onDone, onError, onIntent, onStatus, onMeta } = callbacks let buffer = '' let currentEvent = '' const task = uni.request({ url: BASE_URL + '/chat/stream', method: 'POST', data: { message: data.message, conversation_id: data.conversation_id || '' }, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getToken() }, enableChunked: true, responseType: 'text', success: () => {}, fail: (err) => { if (onError) onError(err) } }) task.onChunkReceived((res) => { try { const chunk = typeof res.data === 'string' ? res.data : new TextDecoder('utf-8').decode(res.data) buffer += chunk const lines = buffer.split('\n') buffer = lines.pop() || '' for (const line of lines) { if (line.startsWith('event: ')) { currentEvent = line.slice(7).trim() continue } if (!line.startsWith('data: ')) continue const d = line.slice(6) if (d === '[DONE]') continue if (currentEvent === 'intent' && onIntent) { onIntent(d) } else if (currentEvent === 'status' && onStatus) { onStatus(d) } else if (currentEvent === 'meta' && onMeta) { try { onMeta(JSON.parse(d)) } catch(e) {} } else if (currentEvent === 'content' || !currentEvent) { if (onToken) onToken(d) } currentEvent = '' } } catch (e) { console.error('SSE parse error:', e) } }) task.onHeadersReceived(() => { // SSE 连接建立 }) return task } /** * 非流式对话(获取来源) */ export function chatAsk(data) { return request('/chat/ask', { method: 'POST', data: { message: data.message, conversation_id: data.conversation_id || '' } }) } /** * 对话历史 */ export function getHistory(page = 1, pageSize = 20) { return request('/chat/history?page=' + page + '&page_size=' + pageSize) } /** * 对话详情 */ export function getConversation(cid) { return request('/chat/history/' + cid) } /** * 提交反馈 */ export function submitFeedback(conversationId, messageId, feedback) { return request('/chat/feedback', { method: 'POST', data: { conversation_id: conversationId, message_id: messageId, feedback } }) } /** * 药品搜索 */ export function searchDrug(keyword, options = {}) { const { category, page = 1, pageSize = 20 } = options let url = '/drug/search?keyword=' + encodeURIComponent(keyword) + '&page=' + page + '&page_size=' + pageSize if (category) url += '&category=' + encodeURIComponent(category) return request(url) } export function getDrugDetail(drugId) { return request('/drug/' + drugId) } export function getCategoryTree() { return request('/drug/category/tree') }