西藏巴青项目

aiLlmChat.js 6.4KB

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