西藏巴青项目

aiOnlineConsult.js 9.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  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. const model =
  100. json.model != null && json.model !== '' ? String(json.model) : null
  101. return { sessionId, delta, model }
  102. } catch (e) {
  103. return null
  104. }
  105. }
  106. function parseChatSseDataLine(line) {
  107. const trimmed = (line || '').trim()
  108. if (!trimmed || trimmed.startsWith(':')) {
  109. return null
  110. }
  111. if (trimmed.startsWith('data:')) {
  112. return parseChatJsonPayload(trimmed.slice(5).trim())
  113. }
  114. if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
  115. return parseChatJsonPayload(trimmed)
  116. }
  117. return null
  118. }
  119. /** 追加 delta;若上游发的是「累计全文」则覆盖而非重复拼接 */
  120. function appendStreamDelta(state, delta) {
  121. if (!delta) {
  122. return
  123. }
  124. const prev = state.fullText || ''
  125. if (!prev) {
  126. state.fullText = delta
  127. return
  128. }
  129. if (delta === prev) {
  130. return
  131. }
  132. if (delta.startsWith(prev)) {
  133. state.fullText = delta
  134. return
  135. }
  136. if (prev.startsWith(delta)) {
  137. return
  138. }
  139. state.fullText += delta
  140. }
  141. function consumeSseLines(state, lines, callbacks) {
  142. for (const line of lines) {
  143. const parsed = parseChatSseDataLine(line)
  144. if (!parsed) {
  145. continue
  146. }
  147. if (parsed.sessionId) {
  148. const changed = state.sessionId !== parsed.sessionId
  149. state.sessionId = parsed.sessionId
  150. if (changed && callbacks.onSessionId) {
  151. callbacks.onSessionId(parsed.sessionId)
  152. }
  153. }
  154. if (parsed.model) {
  155. state.model = parsed.model
  156. if (callbacks.onModel) {
  157. callbacks.onModel(parsed.model)
  158. }
  159. }
  160. if (parsed.delta) {
  161. const before = state.fullText
  162. appendStreamDelta(state, parsed.delta)
  163. if (state.fullText !== before && callbacks.onDelta) {
  164. callbacks.onDelta(state.fullText, parsed.delta)
  165. }
  166. }
  167. }
  168. }
  169. function appendSseChunk(state, chunk, callbacks) {
  170. if (!chunk) {
  171. return
  172. }
  173. const text = String(chunk)
  174. if (!state.buffer && text.trim().startsWith('{') && text.includes('"choices"')) {
  175. const parsed = parseChatJsonPayload(text.trim())
  176. if (parsed && parsed.delta) {
  177. const before = state.fullText
  178. appendStreamDelta(state, parsed.delta)
  179. if (state.fullText !== before && callbacks.onDelta) {
  180. callbacks.onDelta(state.fullText, parsed.delta)
  181. }
  182. if (parsed.sessionId) {
  183. const changed = state.sessionId !== parsed.sessionId
  184. state.sessionId = parsed.sessionId
  185. if (changed && callbacks.onSessionId) {
  186. callbacks.onSessionId(parsed.sessionId)
  187. }
  188. }
  189. if (parsed.model) {
  190. state.model = parsed.model
  191. if (callbacks.onModel) {
  192. callbacks.onModel(parsed.model)
  193. }
  194. }
  195. return
  196. }
  197. }
  198. state.buffer += text
  199. const lines = state.buffer.split(/\r?\n/)
  200. state.buffer = lines.pop() || ''
  201. consumeSseLines(state, lines, callbacks)
  202. }
  203. function flushSseBuffer(state, callbacks) {
  204. if (!state.buffer) {
  205. return
  206. }
  207. const tail = state.buffer
  208. state.buffer = ''
  209. consumeSseLines(state, [tail], callbacks)
  210. }
  211. function decodeResponseChunk(data) {
  212. if (data == null) {
  213. return ''
  214. }
  215. if (typeof data === 'string') {
  216. return data
  217. }
  218. if (data instanceof ArrayBuffer) {
  219. try {
  220. return new TextDecoder('utf-8').decode(data)
  221. } catch (e) {
  222. const u8 = new Uint8Array(data)
  223. let s = ''
  224. for (let i = 0; i < u8.length; i++) {
  225. s += String.fromCharCode(u8[i])
  226. }
  227. try {
  228. return decodeURIComponent(escape(s))
  229. } catch (e2) {
  230. return s
  231. }
  232. }
  233. }
  234. return String(data)
  235. }
  236. function streamRequestHeaders() {
  237. const header = {
  238. 'Content-Type': 'application/json',
  239. Accept: 'text/event-stream',
  240. 'Cache-Control': 'no-cache'
  241. }
  242. const token = getToken()
  243. if (token) {
  244. header.Authorization = 'Bearer ' + token
  245. }
  246. return header
  247. }
  248. function createStreamState(options) {
  249. const { onDelta, onSessionId, onModel } = options
  250. return {
  251. startMs: Date.now(),
  252. buffer: '',
  253. fullText: '',
  254. sessionId: null,
  255. model: null,
  256. chunkReceived: false,
  257. callbacks: { onDelta, onSessionId, onModel }
  258. }
  259. }
  260. function finishStreamState(state) {
  261. flushSseBuffer(state, state.callbacks)
  262. return {
  263. content: state.fullText,
  264. sessionId: state.sessionId,
  265. model: state.model,
  266. durationMs: Date.now() - state.startMs
  267. }
  268. }
  269. // #ifdef H5
  270. async function sendChatMessageStream(body, options) {
  271. const state = createStreamState(options)
  272. const res = await fetch(joinApiUrl('/v1/chat/completions'), {
  273. method: 'POST',
  274. headers: streamRequestHeaders(),
  275. body: JSON.stringify(body),
  276. signal: options.signal
  277. })
  278. if (!res.ok) {
  279. const text = await res.text()
  280. let msg = 'requestFailed'
  281. try {
  282. const err = JSON.parse(text)
  283. msg = (err && (err.message || err.msg)) || msg
  284. } catch (e) {
  285. if (text) {
  286. msg = text.slice(0, 200)
  287. }
  288. }
  289. throw new Error(msg)
  290. }
  291. const reader = res.body && res.body.getReader()
  292. if (!reader) {
  293. throw new Error('requestFailed')
  294. }
  295. const decoder = new TextDecoder()
  296. while (true) {
  297. const { done, value } = await reader.read()
  298. if (done) {
  299. break
  300. }
  301. state.chunkReceived = true
  302. appendSseChunk(state, decoder.decode(value, { stream: true }), state.callbacks)
  303. }
  304. appendSseChunk(state, decoder.decode(), state.callbacks)
  305. return finishStreamState(state)
  306. }
  307. // #endif
  308. // #ifndef H5
  309. function sendChatMessageStream(body, options) {
  310. const state = createStreamState(options)
  311. const url = joinApiUrl('/v1/chat/completions')
  312. return new Promise((resolve, reject) => {
  313. const reqOptions = {
  314. url,
  315. method: 'POST',
  316. header: streamRequestHeaders(),
  317. data: body,
  318. timeout: 600000,
  319. enableChunked: true,
  320. responseType: 'text',
  321. success(res) {
  322. if (!res || res.statusCode < 200 || res.statusCode >= 300) {
  323. let msg = 'requestFailed'
  324. try {
  325. const errBody = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
  326. msg = (errBody && (errBody.message || errBody.msg)) || msg
  327. } catch (e) {
  328. /* ignore */
  329. }
  330. reject(new Error(msg))
  331. return
  332. }
  333. if (!state.chunkReceived && res.data) {
  334. appendSseChunk(state, decodeResponseChunk(res.data), state.callbacks)
  335. }
  336. resolve(finishStreamState(state))
  337. },
  338. fail(err) {
  339. reject(err || new Error('requestFailed'))
  340. }
  341. }
  342. const task = uni.request(reqOptions)
  343. if (options.onReadyTask && task) {
  344. options.onReadyTask(task)
  345. }
  346. if (task && typeof task.onChunkReceived === 'function') {
  347. task.onChunkReceived((res) => {
  348. state.chunkReceived = true
  349. appendSseChunk(state, decodeResponseChunk(res.data), state.callbacks)
  350. })
  351. }
  352. })
  353. }
  354. // #endif
  355. /**
  356. * 聊天 POST /v1/chat/completions(默认 stream: true)
  357. * options: onDelta, onSessionId, onModel, onReadyTask(task), signal(H5)
  358. * 流式 resolve:{ content, sessionId, model, durationMs },durationMs 为整段流式请求耗时(毫秒)
  359. */
  360. export function sendChatMessage(data, options = {}) {
  361. const body = { stream: true, ...(data || {}) }
  362. if (body.stream !== false) {
  363. return sendChatMessageStream(body, options)
  364. }
  365. return request({
  366. url: '/v1/chat/completions',
  367. method: 'POST',
  368. data: body,
  369. timeout: 600000
  370. })
  371. }