西藏巴青项目

aiOnlineConsult.js 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. import request from '@/utils/request'
  2. import { joinApiUrl } from '@/config'
  3. import { getToken } from '@/utils/auth'
  4. const BASE = '/app/consult/ai'
  5. /** 会话列表(含 disclaimer) */
  6. export function listAiConsultSessions(query) {
  7. return request({
  8. url: BASE + '/session/list',
  9. method: 'GET',
  10. data: query
  11. })
  12. }
  13. /** 新建会话,body 可选 { category: 1|2|3|4 } */
  14. export function createAiConsultSession(data) {
  15. return request({
  16. url: BASE + '/session',
  17. method: 'POST',
  18. header: { repeatSubmit: false },
  19. data: data || {}
  20. })
  21. }
  22. /**
  23. * 历史消息(路径参数 sessionId = 若依问诊会话主键,与改造前一致,勿传大模型 sessionId)
  24. */
  25. export function listAiConsultMessages(sessionId, query) {
  26. return request({
  27. url: BASE + '/session/' + sessionId + '/messages',
  28. method: 'GET',
  29. data: query
  30. })
  31. }
  32. /** 提问(服务端同步等待 AI,约 60s) */
  33. export function sendAiConsultMessage(sessionId, data) {
  34. return request({
  35. url: BASE + '/session/' + sessionId + '/message',
  36. method: 'POST',
  37. header: { repeatSubmit: false },
  38. data,
  39. timeout: 65000
  40. })
  41. }
  42. /** 隐藏/删除会话 */
  43. export function hideAiConsultSession(sessionId) {
  44. return request({
  45. url: BASE + '/session/' + sessionId + '/hide',
  46. method: 'POST',
  47. header: { repeatSubmit: false }
  48. })
  49. }
  50. export function getModelList() {
  51. return request({
  52. url: '/v1/models',
  53. method: 'GET'
  54. })
  55. }
  56. /** 从大模型 SSE/JSON 解析会话 id;流式块中的 id 即为 sessionId */
  57. export function extractLlmSessionIdFromJson(json) {
  58. if (!json || typeof json !== 'object') {
  59. return null
  60. }
  61. if (json.id != null && json.id !== '') {
  62. return String(json.id)
  63. }
  64. if (json.session_id != null && json.session_id !== '') {
  65. return String(json.session_id)
  66. }
  67. if (json.sessionId != null && json.sessionId !== '') {
  68. return String(json.sessionId)
  69. }
  70. return null
  71. }
  72. function extractDeltaFromChatJson(json) {
  73. if (!json || typeof json !== 'object') {
  74. return ''
  75. }
  76. const choice = json.choices && json.choices[0]
  77. if (!choice) {
  78. return ''
  79. }
  80. if (choice.delta && choice.delta.content != null) {
  81. return String(choice.delta.content)
  82. }
  83. if (choice.message && choice.message.content != null) {
  84. return String(choice.message.content)
  85. }
  86. if (choice.text != null) {
  87. return String(choice.text)
  88. }
  89. return ''
  90. }
  91. function parseChatJsonPayload(payload) {
  92. if (!payload || payload === '[DONE]') {
  93. return { done: true }
  94. }
  95. try {
  96. const json = JSON.parse(payload)
  97. const sessionId = extractLlmSessionIdFromJson(json)
  98. const delta = extractDeltaFromChatJson(json)
  99. return { sessionId, delta }
  100. } catch (e) {
  101. return null
  102. }
  103. }
  104. function parseChatSseDataLine(line) {
  105. const trimmed = (line || '').trim()
  106. if (!trimmed || trimmed.startsWith(':')) {
  107. return null
  108. }
  109. if (trimmed.startsWith('data:')) {
  110. return parseChatJsonPayload(trimmed.slice(5).trim())
  111. }
  112. if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
  113. return parseChatJsonPayload(trimmed)
  114. }
  115. return null
  116. }
  117. /** 追加 delta;若上游发的是「累计全文」则覆盖而非重复拼接 */
  118. function appendStreamDelta(state, delta) {
  119. if (!delta) {
  120. return
  121. }
  122. const prev = state.fullText || ''
  123. if (!prev) {
  124. state.fullText = delta
  125. return
  126. }
  127. if (delta === prev) {
  128. return
  129. }
  130. if (delta.startsWith(prev)) {
  131. state.fullText = delta
  132. return
  133. }
  134. if (prev.startsWith(delta)) {
  135. return
  136. }
  137. state.fullText += delta
  138. }
  139. function consumeSseLines(state, lines, callbacks) {
  140. for (const line of lines) {
  141. const parsed = parseChatSseDataLine(line)
  142. if (!parsed) {
  143. continue
  144. }
  145. if (parsed.sessionId) {
  146. const changed = state.sessionId !== parsed.sessionId
  147. state.sessionId = parsed.sessionId
  148. if (changed && callbacks.onSessionId) {
  149. callbacks.onSessionId(parsed.sessionId)
  150. }
  151. }
  152. if (parsed.delta) {
  153. const before = state.fullText
  154. appendStreamDelta(state, parsed.delta)
  155. if (state.fullText !== before && callbacks.onDelta) {
  156. callbacks.onDelta(state.fullText, parsed.delta)
  157. }
  158. }
  159. }
  160. }
  161. function appendSseChunk(state, chunk, callbacks) {
  162. if (!chunk) {
  163. return
  164. }
  165. const text = String(chunk)
  166. if (!state.buffer && text.trim().startsWith('{') && text.includes('"choices"')) {
  167. const parsed = parseChatJsonPayload(text.trim())
  168. if (parsed && parsed.delta) {
  169. const before = state.fullText
  170. appendStreamDelta(state, parsed.delta)
  171. if (state.fullText !== before && callbacks.onDelta) {
  172. callbacks.onDelta(state.fullText, parsed.delta)
  173. }
  174. if (parsed.sessionId) {
  175. const changed = state.sessionId !== parsed.sessionId
  176. state.sessionId = parsed.sessionId
  177. if (changed && callbacks.onSessionId) {
  178. callbacks.onSessionId(parsed.sessionId)
  179. }
  180. }
  181. return
  182. }
  183. }
  184. state.buffer += text
  185. const lines = state.buffer.split(/\r?\n/)
  186. state.buffer = lines.pop() || ''
  187. consumeSseLines(state, lines, callbacks)
  188. }
  189. function flushSseBuffer(state, callbacks) {
  190. if (!state.buffer) {
  191. return
  192. }
  193. const tail = state.buffer
  194. state.buffer = ''
  195. consumeSseLines(state, [tail], callbacks)
  196. }
  197. function decodeResponseChunk(data) {
  198. if (data == null) {
  199. return ''
  200. }
  201. if (typeof data === 'string') {
  202. return data
  203. }
  204. if (data instanceof ArrayBuffer) {
  205. try {
  206. return new TextDecoder('utf-8').decode(data)
  207. } catch (e) {
  208. const u8 = new Uint8Array(data)
  209. let s = ''
  210. for (let i = 0; i < u8.length; i++) {
  211. s += String.fromCharCode(u8[i])
  212. }
  213. try {
  214. return decodeURIComponent(escape(s))
  215. } catch (e2) {
  216. return s
  217. }
  218. }
  219. }
  220. return String(data)
  221. }
  222. function streamRequestHeaders() {
  223. const header = {
  224. 'Content-Type': 'application/json',
  225. Accept: 'text/event-stream',
  226. 'Cache-Control': 'no-cache'
  227. }
  228. const token = getToken()
  229. if (token) {
  230. header.Authorization = 'Bearer ' + token
  231. }
  232. return header
  233. }
  234. function createStreamState(options) {
  235. const { onDelta, onSessionId } = options
  236. return {
  237. startMs: Date.now(),
  238. buffer: '',
  239. fullText: '',
  240. sessionId: null,
  241. chunkReceived: false,
  242. callbacks: { onDelta, onSessionId }
  243. }
  244. }
  245. function finishStreamState(state) {
  246. flushSseBuffer(state, state.callbacks)
  247. return {
  248. content: state.fullText,
  249. sessionId: state.sessionId,
  250. durationMs: Date.now() - state.startMs
  251. }
  252. }
  253. // #ifdef H5
  254. async function sendChatMessageStream(body, options) {
  255. const state = createStreamState(options)
  256. const res = await fetch(joinApiUrl('/v1/chat/completions'), {
  257. method: 'POST',
  258. headers: streamRequestHeaders(),
  259. body: JSON.stringify(body),
  260. signal: options.signal
  261. })
  262. if (!res.ok) {
  263. const text = await res.text()
  264. let msg = 'requestFailed'
  265. try {
  266. const err = JSON.parse(text)
  267. msg = (err && (err.message || err.msg)) || msg
  268. } catch (e) {
  269. if (text) {
  270. msg = text.slice(0, 200)
  271. }
  272. }
  273. throw new Error(msg)
  274. }
  275. const reader = res.body && res.body.getReader()
  276. if (!reader) {
  277. throw new Error('requestFailed')
  278. }
  279. const decoder = new TextDecoder()
  280. while (true) {
  281. const { done, value } = await reader.read()
  282. if (done) {
  283. break
  284. }
  285. state.chunkReceived = true
  286. appendSseChunk(state, decoder.decode(value, { stream: true }), state.callbacks)
  287. }
  288. appendSseChunk(state, decoder.decode(), state.callbacks)
  289. return finishStreamState(state)
  290. }
  291. // #endif
  292. // #ifndef H5
  293. function sendChatMessageStream(body, options) {
  294. const state = createStreamState(options)
  295. const url = joinApiUrl('/v1/chat/completions')
  296. return new Promise((resolve, reject) => {
  297. const reqOptions = {
  298. url,
  299. method: 'POST',
  300. header: streamRequestHeaders(),
  301. data: body,
  302. timeout: 600000,
  303. enableChunked: true,
  304. responseType: 'text',
  305. success(res) {
  306. if (!res || res.statusCode < 200 || res.statusCode >= 300) {
  307. let msg = 'requestFailed'
  308. try {
  309. const errBody = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
  310. msg = (errBody && (errBody.message || errBody.msg)) || msg
  311. } catch (e) {
  312. /* ignore */
  313. }
  314. reject(new Error(msg))
  315. return
  316. }
  317. if (!state.chunkReceived && res.data) {
  318. appendSseChunk(state, decodeResponseChunk(res.data), state.callbacks)
  319. }
  320. resolve(finishStreamState(state))
  321. },
  322. fail(err) {
  323. reject(err || new Error('requestFailed'))
  324. }
  325. }
  326. const task = uni.request(reqOptions)
  327. if (options.onReadyTask && task) {
  328. options.onReadyTask(task)
  329. }
  330. if (task && typeof task.onChunkReceived === 'function') {
  331. task.onChunkReceived((res) => {
  332. state.chunkReceived = true
  333. appendSseChunk(state, decodeResponseChunk(res.data), state.callbacks)
  334. })
  335. }
  336. })
  337. }
  338. // #endif
  339. /**
  340. * 聊天 POST /v1/chat/completions(默认 stream: true)
  341. * options: onDelta, onSessionId, onReadyTask(task), signal(H5)
  342. * 流式 resolve:{ content, sessionId, durationMs },durationMs 为整段流式请求耗时(毫秒)
  343. */
  344. export function sendChatMessage(data, options = {}) {
  345. const body = { stream: true, ...(data || {}) }
  346. if (body.stream !== false) {
  347. return sendChatMessageStream(body, options)
  348. }
  349. return request({
  350. url: '/v1/chat/completions',
  351. method: 'POST',
  352. data: body,
  353. timeout: 600000
  354. })
  355. }