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 }) }