| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- import request from '@/utils/request'
- import { joinApiUrl } from '@/config'
- import { getToken } from '@/utils/auth'
- const BASE = '/app/consult/ai'
- /** 会话列表(含 disclaimer) */
- export function listAiConsultSessions(query) {
- return request({
- url: BASE + '/session/list',
- method: 'GET',
- data: query
- })
- }
- /** 新建会话,body 可选 { category: 1|2|3|4 } */
- export function createAiConsultSession(data) {
- return request({
- url: BASE + '/session',
- method: 'POST',
- header: { repeatSubmit: false },
- data: data || {}
- })
- }
- /**
- * 历史消息(路径参数 sessionId = 若依问诊会话主键,与改造前一致,勿传大模型 sessionId)
- */
- export function listAiConsultMessages(sessionId, query) {
- return request({
- url: BASE + '/session/' + sessionId + '/messages',
- method: 'GET',
- data: query
- })
- }
- /** 提问(服务端同步等待 AI,约 60s) */
- export function sendAiConsultMessage(sessionId, data) {
- return request({
- url: BASE + '/session/' + sessionId + '/message',
- method: 'POST',
- header: { repeatSubmit: false },
- data,
- timeout: 65000
- })
- }
- /** 隐藏/删除会话 */
- export function hideAiConsultSession(sessionId) {
- return request({
- url: BASE + '/session/' + sessionId + '/hide',
- method: 'POST',
- header: { repeatSubmit: false }
- })
- }
- export function getModelList() {
- return request({
- url: '/v1/models',
- method: 'GET'
- })
- }
- /** 从大模型 SSE/JSON 解析会话 id;流式块中的 id 即为 sessionId */
- export function extractLlmSessionIdFromJson(json) {
- if (!json || typeof json !== 'object') {
- return null
- }
- if (json.id != null && json.id !== '') {
- return String(json.id)
- }
- if (json.session_id != null && json.session_id !== '') {
- return String(json.session_id)
- }
- if (json.sessionId != null && json.sessionId !== '') {
- return String(json.sessionId)
- }
- return null
- }
- function extractDeltaFromChatJson(json) {
- if (!json || typeof json !== 'object') {
- return ''
- }
- const choice = json.choices && json.choices[0]
- if (!choice) {
- return ''
- }
- if (choice.delta && choice.delta.content != null) {
- return String(choice.delta.content)
- }
- if (choice.message && choice.message.content != null) {
- return String(choice.message.content)
- }
- if (choice.text != null) {
- return String(choice.text)
- }
- return ''
- }
- function parseChatJsonPayload(payload) {
- if (!payload || payload === '[DONE]') {
- return { done: true }
- }
- try {
- const json = JSON.parse(payload)
- const sessionId = extractLlmSessionIdFromJson(json)
- const delta = extractDeltaFromChatJson(json)
- const model =
- json.model != null && json.model !== '' ? String(json.model) : null
- return { sessionId, delta, model }
- } catch (e) {
- return null
- }
- }
- function parseChatSseDataLine(line) {
- const trimmed = (line || '').trim()
- if (!trimmed || trimmed.startsWith(':')) {
- return null
- }
- if (trimmed.startsWith('data:')) {
- return parseChatJsonPayload(trimmed.slice(5).trim())
- }
- if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
- return parseChatJsonPayload(trimmed)
- }
- return null
- }
- /** 追加 delta;若上游发的是「累计全文」则覆盖而非重复拼接 */
- function appendStreamDelta(state, delta) {
- if (!delta) {
- return
- }
- const prev = state.fullText || ''
- if (!prev) {
- state.fullText = delta
- return
- }
- if (delta === prev) {
- return
- }
- if (delta.startsWith(prev)) {
- state.fullText = delta
- return
- }
- if (prev.startsWith(delta)) {
- return
- }
- state.fullText += delta
- }
- function consumeSseLines(state, lines, callbacks) {
- for (const line of lines) {
- const parsed = parseChatSseDataLine(line)
- if (!parsed) {
- continue
- }
- if (parsed.sessionId) {
- const changed = state.sessionId !== parsed.sessionId
- state.sessionId = parsed.sessionId
- if (changed && callbacks.onSessionId) {
- callbacks.onSessionId(parsed.sessionId)
- }
- }
- if (parsed.model) {
- state.model = parsed.model
- if (callbacks.onModel) {
- callbacks.onModel(parsed.model)
- }
- }
- if (parsed.delta) {
- const before = state.fullText
- appendStreamDelta(state, parsed.delta)
- if (state.fullText !== before && callbacks.onDelta) {
- callbacks.onDelta(state.fullText, parsed.delta)
- }
- }
- }
- }
- function appendSseChunk(state, chunk, callbacks) {
- if (!chunk) {
- return
- }
- const text = String(chunk)
- if (!state.buffer && text.trim().startsWith('{') && text.includes('"choices"')) {
- const parsed = parseChatJsonPayload(text.trim())
- if (parsed && parsed.delta) {
- const before = state.fullText
- appendStreamDelta(state, parsed.delta)
- if (state.fullText !== before && callbacks.onDelta) {
- callbacks.onDelta(state.fullText, parsed.delta)
- }
- if (parsed.sessionId) {
- const changed = state.sessionId !== parsed.sessionId
- state.sessionId = parsed.sessionId
- if (changed && callbacks.onSessionId) {
- callbacks.onSessionId(parsed.sessionId)
- }
- }
- if (parsed.model) {
- state.model = parsed.model
- if (callbacks.onModel) {
- callbacks.onModel(parsed.model)
- }
- }
- return
- }
- }
- state.buffer += text
- const lines = state.buffer.split(/\r?\n/)
- state.buffer = lines.pop() || ''
- consumeSseLines(state, lines, callbacks)
- }
- function flushSseBuffer(state, callbacks) {
- if (!state.buffer) {
- return
- }
- const tail = state.buffer
- state.buffer = ''
- consumeSseLines(state, [tail], callbacks)
- }
- function decodeResponseChunk(data) {
- if (data == null) {
- return ''
- }
- if (typeof data === 'string') {
- return data
- }
- if (data instanceof ArrayBuffer) {
- try {
- return new TextDecoder('utf-8').decode(data)
- } catch (e) {
- const u8 = new Uint8Array(data)
- let s = ''
- for (let i = 0; i < u8.length; i++) {
- s += String.fromCharCode(u8[i])
- }
- try {
- return decodeURIComponent(escape(s))
- } catch (e2) {
- return s
- }
- }
- }
- return String(data)
- }
- function streamRequestHeaders() {
- const header = {
- 'Content-Type': 'application/json',
- Accept: 'text/event-stream',
- 'Cache-Control': 'no-cache'
- }
- const token = getToken()
- if (token) {
- header.Authorization = 'Bearer ' + token
- }
- return header
- }
- function createStreamState(options) {
- const { onDelta, onSessionId, onModel } = options
- return {
- startMs: Date.now(),
- buffer: '',
- fullText: '',
- sessionId: null,
- model: null,
- chunkReceived: false,
- callbacks: { onDelta, onSessionId, onModel }
- }
- }
- function finishStreamState(state) {
- flushSseBuffer(state, state.callbacks)
- return {
- content: state.fullText,
- sessionId: state.sessionId,
- model: state.model,
- durationMs: Date.now() - state.startMs
- }
- }
- // #ifdef H5
- async function sendChatMessageStream(body, options) {
- const state = createStreamState(options)
- const res = await fetch(joinApiUrl('/v1/chat/completions'), {
- method: 'POST',
- headers: streamRequestHeaders(),
- body: JSON.stringify(body),
- signal: options.signal
- })
- if (!res.ok) {
- const text = await res.text()
- let msg = 'requestFailed'
- try {
- const err = JSON.parse(text)
- msg = (err && (err.message || err.msg)) || msg
- } catch (e) {
- if (text) {
- msg = text.slice(0, 200)
- }
- }
- throw new Error(msg)
- }
- const reader = res.body && res.body.getReader()
- if (!reader) {
- throw new Error('requestFailed')
- }
- const decoder = new TextDecoder()
- while (true) {
- const { done, value } = await reader.read()
- if (done) {
- break
- }
- state.chunkReceived = true
- appendSseChunk(state, decoder.decode(value, { stream: true }), state.callbacks)
- }
- appendSseChunk(state, decoder.decode(), state.callbacks)
- return finishStreamState(state)
- }
- // #endif
- // #ifndef H5
- function sendChatMessageStream(body, options) {
- const state = createStreamState(options)
- const url = joinApiUrl('/v1/chat/completions')
- return new Promise((resolve, reject) => {
- const reqOptions = {
- url,
- method: 'POST',
- header: streamRequestHeaders(),
- data: body,
- timeout: 600000,
- enableChunked: true,
- responseType: 'text',
- success(res) {
- if (!res || res.statusCode < 200 || res.statusCode >= 300) {
- let msg = 'requestFailed'
- try {
- const errBody = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
- msg = (errBody && (errBody.message || errBody.msg)) || msg
- } catch (e) {
- /* ignore */
- }
- reject(new Error(msg))
- return
- }
- if (!state.chunkReceived && res.data) {
- appendSseChunk(state, decodeResponseChunk(res.data), state.callbacks)
- }
- resolve(finishStreamState(state))
- },
- fail(err) {
- reject(err || new Error('requestFailed'))
- }
- }
- const task = uni.request(reqOptions)
- if (options.onReadyTask && task) {
- options.onReadyTask(task)
- }
- if (task && typeof task.onChunkReceived === 'function') {
- task.onChunkReceived((res) => {
- state.chunkReceived = true
- appendSseChunk(state, decodeResponseChunk(res.data), state.callbacks)
- })
- }
- })
- }
- // #endif
- /**
- * 聊天 POST /v1/chat/completions(默认 stream: true)
- * options: onDelta, onSessionId, onModel, onReadyTask(task), signal(H5)
- * 流式 resolve:{ content, sessionId, model, durationMs },durationMs 为整段流式请求耗时(毫秒)
- */
- export function sendChatMessage(data, options = {}) {
- const body = { stream: true, ...(data || {}) }
- if (body.stream !== false) {
- return sendChatMessageStream(body, options)
- }
- return request({
- url: '/v1/chat/completions',
- method: 'POST',
- data: body,
- timeout: 600000
- })
- }
|