西藏巴青项目

aiLlmChat.js 6.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import { joinLlmUrl, LLM_API_KEY } from '@/config/llm'
  2. import { mediaUrl, SENDER_ROLE_USER, SENDER_ROLE_AI } from '@/utils/aiConsult'
  3. export { SENDER_ROLE_USER, SENDER_ROLE_AI }
  4. export const MODEL_OPTION_DEFS = [
  5. { value: 'auto', labelKey: 'modelAuto', shortKey: 'modelAutoShort', descKey: 'modelAutoDesc', icon: 'grid-fill' },
  6. { value: 'yak-disease', labelKey: 'modelDisease', shortKey: 'modelDiseaseShort', descKey: 'modelDiseaseDesc', icon: 'order' },
  7. { value: 'yak-general', labelKey: 'modelGeneral', shortKey: 'modelGeneralShort', descKey: 'modelGeneralDesc', icon: 'chat' },
  8. { value: 'yak-feeding', labelKey: 'modelFeeding', shortKey: 'modelFeedingShort', descKey: 'modelFeedingDesc', icon: 'shopping-cart' }
  9. ]
  10. export const MEDIA_RULES = {
  11. image: { exts: ['jpg', 'jpeg', 'png', 'gif'], maxMb: 10, errFmt: 'errImageFmt', errMb: 'errImageMb' },
  12. video: { exts: ['mp4', 'mov'], maxMb: 50, errFmt: 'errVideoFmt', errMb: 'errVideoMb' },
  13. voice: { exts: ['mp3', 'm4a', 'wav'], maxMb: 10, errFmt: 'errVoiceFmt', errMb: 'errVoiceMb' }
  14. }
  15. export function genLocalId() {
  16. return 'm_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 9)
  17. }
  18. export function extOf(fileName) {
  19. if (!fileName || fileName.lastIndexOf('.') < 0) {
  20. return ''
  21. }
  22. return fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase()
  23. }
  24. /** 与大模型 SSE/JSON 一致:id 即为网关 sessionId */
  25. export function extractLlmSessionId(data) {
  26. if (!data || typeof data !== 'object') {
  27. return null
  28. }
  29. if (data.id != null && data.id !== '') {
  30. return String(data.id)
  31. }
  32. if (data.session_id != null && data.session_id !== '') {
  33. return String(data.session_id)
  34. }
  35. if (data.sessionId != null && data.sessionId !== '') {
  36. return String(data.sessionId)
  37. }
  38. return null
  39. }
  40. export function extractAssistantText(data) {
  41. try {
  42. const choice = data.choices && data.choices[0]
  43. const msg = choice && choice.message
  44. return (msg && msg.content) || ''
  45. } catch (e) {
  46. return ''
  47. }
  48. }
  49. export function contentForLlm(content) {
  50. if (typeof content === 'string') {
  51. return content
  52. }
  53. if (!Array.isArray(content)) {
  54. return ''
  55. }
  56. return content
  57. }
  58. export function buildUserContentForLlm(text, pendingAttachments) {
  59. const parts = []
  60. const imgs = (pendingAttachments || []).filter((a) => a.kind === 'image')
  61. for (const im of imgs) {
  62. parts.push({ type: 'image_url', image_url: { url: im.url } })
  63. }
  64. const body = (text || '').trim()
  65. if (body) {
  66. parts.push({ type: 'text', text: body })
  67. }
  68. if (!parts.length) {
  69. return ''
  70. }
  71. if (parts.length === 1 && parts[0].type === 'text') {
  72. return parts[0].text
  73. }
  74. return parts
  75. }
  76. export function buildMessagesForLlm(messages, llmSessionId) {
  77. if (llmSessionId) {
  78. for (let i = (messages || []).length - 1; i >= 0; i--) {
  79. const m = messages[i]
  80. if (m.senderRole === SENDER_ROLE_USER) {
  81. let content = m.content
  82. if (m.msgType === 2 && content) {
  83. content = [{ type: 'image_url', image_url: { url: mediaUrl(content) } }]
  84. }
  85. return [{ role: 'user', content: contentForLlm(content) }]
  86. }
  87. }
  88. return []
  89. }
  90. const out = []
  91. for (const m of messages || []) {
  92. const role = m.senderRole === SENDER_ROLE_AI ? 'assistant' : 'user'
  93. let content = m.content
  94. if (m.msgType === 2 && content) {
  95. content = [{ type: 'image_url', image_url: { url: mediaUrl(content) } }]
  96. }
  97. out.push({ role, content: contentForLlm(content) })
  98. }
  99. return out
  100. }
  101. export function resolvePayloadUserContent(payload, draft) {
  102. if (payload.msgType === 1) {
  103. return payload.content
  104. }
  105. if (payload.msgType === 2) {
  106. const parts = [{ type: 'image_url', image_url: { url: mediaUrl(payload.content) } }]
  107. const text = (draft || '').trim()
  108. if (text) {
  109. parts.push({ type: 'text', text })
  110. }
  111. return parts.length === 1 && parts[0].type === 'text' ? parts[0].text : parts
  112. }
  113. return String(payload.content || '')
  114. }
  115. export function buildPersistSendBody(msgType, payload, draftText, pendingAttachments) {
  116. const body = { msgType: msgType || 1 }
  117. if (body.msgType === 1) {
  118. let text =
  119. (draftText || '').trim() || (payload && payload.content ? String(payload.content).trim() : '')
  120. if (!text && pendingAttachments && pendingAttachments.length) {
  121. const img = pendingAttachments.find((a) => a.kind === 'image')
  122. if (img && img.url) {
  123. body.msgType = 2
  124. body.content = img.url
  125. return body
  126. }
  127. }
  128. body.content = text
  129. } else {
  130. body.content = (payload && payload.content) || ''
  131. if (payload && payload.mediaDuration != null) {
  132. body.mediaDuration = payload.mediaDuration
  133. }
  134. }
  135. return body
  136. }
  137. export function findLastLocalExchangeIndexes(messages) {
  138. let userIdx = -1
  139. let aiIdx = -1
  140. for (let i = (messages || []).length - 1; i >= 0; i--) {
  141. const m = messages[i]
  142. if (!m || !String(m.id).startsWith('m_')) {
  143. continue
  144. }
  145. if (aiIdx < 0 && m.senderRole === SENDER_ROLE_AI) {
  146. aiIdx = i
  147. } else if (userIdx < 0 && m.senderRole === SENDER_ROLE_USER) {
  148. userIdx = i
  149. }
  150. if (userIdx >= 0 && aiIdx >= 0) {
  151. break
  152. }
  153. }
  154. return { userIdx, aiIdx }
  155. }
  156. /**
  157. * 调用大模型 /v1/chat/completions
  158. */
  159. export function requestLlmChat(body) {
  160. const baseUrl = joinLlmUrl('')
  161. if (!baseUrl) {
  162. return Promise.reject(new Error('configLlmBase'))
  163. }
  164. if (!LLM_API_KEY) {
  165. return Promise.reject(new Error('configLlmKey'))
  166. }
  167. const header = {
  168. 'Content-Type': 'application/json',
  169. Authorization: 'Bearer ' + LLM_API_KEY
  170. }
  171. return new Promise((resolve, reject) => {
  172. uni.request({
  173. url: joinLlmUrl('/v1/chat/completions'),
  174. method: 'POST',
  175. header,
  176. data: body,
  177. timeout: 120000,
  178. success: (res) => {
  179. const httpStatus = res.statusCode || 200
  180. if (httpStatus >= 200 && httpStatus < 300) {
  181. const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
  182. resolve(data)
  183. return
  184. }
  185. let msg = 'requestFailed'
  186. try {
  187. const errBody = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
  188. msg = (errBody && (errBody.message || errBody.msg)) || msg
  189. } catch (e) {
  190. /* ignore */
  191. }
  192. reject(new Error(msg))
  193. },
  194. fail: (err) => reject(err || new Error('requestFailed'))
  195. })
  196. })
  197. }