ai.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. /**
  2. * AI 模块 API 封装
  3. * SSE 流式对话 — 支持 intent / status / content / meta 多事件
  4. */
  5. const BASE_URL = 'https://your-api-domain.com/api/v1'
  6. // 本地调试: const BASE_URL = 'http://localhost:8000/api/v1'
  7. function getToken() {
  8. return uni.getStorageSync('access_token') || ''
  9. }
  10. function request(url, options = {}) {
  11. return new Promise((resolve, reject) => {
  12. uni.request({
  13. url: BASE_URL + url,
  14. method: options.method || 'GET',
  15. data: options.data,
  16. header: {
  17. 'Content-Type': 'application/json',
  18. 'Authorization': 'Bearer ' + getToken()
  19. },
  20. success: (res) => {
  21. if (res.statusCode === 200) resolve(res.data)
  22. else if (res.statusCode === 401) {
  23. uni.navigateTo({ url: '/pages/login/index' })
  24. reject(new Error('Unauthorized'))
  25. } else reject(new Error(res.data?.detail || 'HTTP ' + res.statusCode))
  26. },
  27. fail: (err) => reject(err)
  28. })
  29. })
  30. }
  31. /**
  32. * SSE 流式对话(支持多事件类型)
  33. * callbacks: { onIntent, onStatus, onToken, onMeta, onDone, onError }
  34. */
  35. export function chatStream(data, callbacks) {
  36. const { onToken, onDone, onError, onIntent, onStatus, onMeta } = callbacks
  37. let buffer = ''
  38. let currentEvent = ''
  39. // 自动选择端点:有媒体附件走 stream-multimodal,纯文本走 stream
  40. const hasMedia = !!(data.media_base64 && data.media_type)
  41. const endpoint = hasMedia ? '/chat/stream-multimodal' : '/chat/stream'
  42. const reqData = {
  43. message: data.message || '',
  44. conversation_id: data.conversation_id || ''
  45. }
  46. if (hasMedia) {
  47. reqData.media_type = data.media_type
  48. reqData.media_base64 = data.media_base64
  49. reqData.media_mime = data.media_mime || ''
  50. }
  51. const task = uni.request({
  52. url: BASE_URL + endpoint,
  53. method: 'POST',
  54. data: reqData,
  55. header: {
  56. 'Content-Type': 'application/json',
  57. 'Authorization': 'Bearer ' + getToken()
  58. },
  59. enableChunked: true,
  60. responseType: 'text',
  61. success: () => {},
  62. fail: (err) => { if (onError) onError(err) }
  63. })
  64. task.onChunkReceived((res) => {
  65. try {
  66. const chunk = typeof res.data === 'string'
  67. ? res.data
  68. : new TextDecoder('utf-8').decode(res.data)
  69. buffer += chunk
  70. const lines = buffer.split('\n')
  71. buffer = lines.pop() || ''
  72. for (const line of lines) {
  73. if (line.startsWith('event: ')) {
  74. currentEvent = line.slice(7).trim()
  75. continue
  76. }
  77. if (!line.startsWith('data: ')) continue
  78. const d = line.slice(6)
  79. if (d === '[DONE]') continue
  80. if (currentEvent === 'intent' && onIntent) {
  81. onIntent(d)
  82. } else if (currentEvent === 'status' && onStatus) {
  83. onStatus(d)
  84. } else if (currentEvent === 'meta' && onMeta) {
  85. try { onMeta(JSON.parse(d)) } catch(e) {}
  86. } else if (currentEvent === 'content' || !currentEvent) {
  87. if (onToken) onToken(d)
  88. }
  89. currentEvent = ''
  90. }
  91. } catch (e) {
  92. console.error('SSE parse error:', e)
  93. }
  94. })
  95. task.onHeadersReceived(() => {
  96. // SSE 连接建立
  97. })
  98. return task
  99. }
  100. /**
  101. * 非流式对话(获取来源)
  102. */
  103. export function chatAsk(data) {
  104. return request('/chat/ask', {
  105. method: 'POST',
  106. data: {
  107. message: data.message,
  108. conversation_id: data.conversation_id || ''
  109. }
  110. })
  111. }
  112. /**
  113. * 对话历史
  114. */
  115. export function getHistory(page = 1, pageSize = 20) {
  116. return request('/chat/history?page=' + page + '&page_size=' + pageSize)
  117. }
  118. /**
  119. * 对话详情
  120. */
  121. export function getConversation(cid) {
  122. return request('/chat/history/' + cid)
  123. }
  124. /**
  125. * 提交反馈
  126. */
  127. export function submitFeedback(conversationId, messageId, feedback) {
  128. return request('/chat/feedback', {
  129. method: 'POST',
  130. data: { conversation_id: conversationId, message_id: messageId, feedback }
  131. })
  132. }
  133. /**
  134. * 药品搜索
  135. */
  136. export function searchDrug(keyword, options = {}) {
  137. const { category, page = 1, pageSize = 20 } = options
  138. let url = '/drug/search?keyword=' + encodeURIComponent(keyword) +
  139. '&page=' + page + '&page_size=' + pageSize
  140. if (category) url += '&category=' + encodeURIComponent(category)
  141. return request(url)
  142. }
  143. export function getDrugDetail(drugId) { return request('/drug/' + drugId) }
  144. export function getCategoryTree() { return request('/drug/category/tree') }