ai.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  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. const task = uni.request({
  40. url: BASE_URL + '/chat/stream',
  41. method: 'POST',
  42. data: {
  43. message: data.message,
  44. conversation_id: data.conversation_id || ''
  45. },
  46. header: {
  47. 'Content-Type': 'application/json',
  48. 'Authorization': 'Bearer ' + getToken()
  49. },
  50. enableChunked: true,
  51. responseType: 'text',
  52. success: () => {},
  53. fail: (err) => { if (onError) onError(err) }
  54. })
  55. task.onChunkReceived((res) => {
  56. try {
  57. const chunk = typeof res.data === 'string'
  58. ? res.data
  59. : new TextDecoder('utf-8').decode(res.data)
  60. buffer += chunk
  61. const lines = buffer.split('\n')
  62. buffer = lines.pop() || ''
  63. for (const line of lines) {
  64. if (line.startsWith('event: ')) {
  65. currentEvent = line.slice(7).trim()
  66. continue
  67. }
  68. if (!line.startsWith('data: ')) continue
  69. const d = line.slice(6)
  70. if (d === '[DONE]') continue
  71. if (currentEvent === 'intent' && onIntent) {
  72. onIntent(d)
  73. } else if (currentEvent === 'status' && onStatus) {
  74. onStatus(d)
  75. } else if (currentEvent === 'meta' && onMeta) {
  76. try { onMeta(JSON.parse(d)) } catch(e) {}
  77. } else if (currentEvent === 'content' || !currentEvent) {
  78. if (onToken) onToken(d)
  79. }
  80. currentEvent = ''
  81. }
  82. } catch (e) {
  83. console.error('SSE parse error:', e)
  84. }
  85. })
  86. task.onHeadersReceived(() => {
  87. // SSE 连接建立
  88. })
  89. return task
  90. }
  91. /**
  92. * 非流式对话(获取来源)
  93. */
  94. export function chatAsk(data) {
  95. return request('/chat/ask', {
  96. method: 'POST',
  97. data: {
  98. message: data.message,
  99. conversation_id: data.conversation_id || ''
  100. }
  101. })
  102. }
  103. /**
  104. * 对话历史
  105. */
  106. export function getHistory(page = 1, pageSize = 20) {
  107. return request('/chat/history?page=' + page + '&page_size=' + pageSize)
  108. }
  109. /**
  110. * 对话详情
  111. */
  112. export function getConversation(cid) {
  113. return request('/chat/history/' + cid)
  114. }
  115. /**
  116. * 提交反馈
  117. */
  118. export function submitFeedback(conversationId, messageId, feedback) {
  119. return request('/chat/feedback', {
  120. method: 'POST',
  121. data: { conversation_id: conversationId, message_id: messageId, feedback }
  122. })
  123. }
  124. /**
  125. * 药品搜索
  126. */
  127. export function searchDrug(keyword, options = {}) {
  128. const { category, page = 1, pageSize = 20 } = options
  129. let url = '/drug/search?keyword=' + encodeURIComponent(keyword) +
  130. '&page=' + page + '&page_size=' + pageSize
  131. if (category) url += '&category=' + encodeURIComponent(category)
  132. return request(url)
  133. }
  134. export function getDrugDetail(drugId) { return request('/drug/' + drugId) }
  135. export function getCategoryTree() { return request('/drug/category/tree') }