This commit is contained in:
Your Name
2026-05-29 09:28:38 +08:00
parent 7b46204454
commit 94e787ae80
11 changed files with 1834 additions and 0 deletions
@@ -0,0 +1,168 @@
/** 拼接 API 根路径与相对路径,避免双斜杠 */
export function joinApiUrl(base, path) {
const b = String(base || '').replace(/\/+$/, '')
const p = String(path || '').replace(/^\/+/, '')
return p ? `${b}/${p}` : b
}
function appendQuery(url, data) {
if (!data || typeof data !== 'object') return url
const keys = Object.keys(data)
if (!keys.length) return url
const qs = keys
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(data[k] ?? '')}`)
.join('&')
return url + (url.includes('?') ? '&' : '?') + qs
}
function decodeChunkData(data) {
if (typeof data === 'string') return data
if (!data) return ''
if (typeof TextDecoder !== 'undefined' && data instanceof ArrayBuffer) {
return new TextDecoder('utf-8').decode(new Uint8Array(data))
}
if (data instanceof ArrayBuffer) {
const bytes = new Uint8Array(data)
let str = ''
for (let i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i])
try {
return decodeURIComponent(escape(str))
} catch (e) {
return str
}
}
return ''
}
/** 从 SSE 文本缓冲中解析并消费完整事件 */
export function consumeSseBuffer(buffer, onEvent) {
let rest = buffer
let idx = rest.indexOf('\n\n')
while (idx !== -1) {
const block = rest.slice(0, idx)
rest = rest.slice(idx + 2)
let event = 'message'
let dataLine = ''
block.split('\n').forEach((line) => {
if (line.startsWith('event:')) event = line.slice(6).trim()
else if (line.startsWith('data:')) dataLine += line.slice(5).trim()
})
if (dataLine) {
try {
onEvent(event, JSON.parse(dataLine))
} catch (e) {
onEvent(event, dataLine)
}
}
idx = rest.indexOf('\n\n')
}
return rest
}
/** 从流式纯文本中提取三餐(支持未写完的最后一行) */
export function parsePartialDietPlainText(text) {
const out = {}
if (!text) return out
const lineRe = /^(早餐|午餐|晚餐|提示|少碰)[:]\s*(.*)$/gm
let match
while ((match = lineRe.exec(text)) !== null) {
const key = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[match[1]]
const val = (match[2] || '').trim()
if (key === 'avoid') {
out.avoid = val.split(/[、,;\s]+/).map((s) => s.trim()).filter(Boolean)
} else {
out[key] = val
}
}
const tail = text.match(/(?:^|\n)(早餐|午餐|晚餐|提示|少碰)[:]\s*([^\n]*)$/)
if (tail) {
const key = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[tail[1]]
const val = (tail[2] || '').trim()
if (key === 'avoid') {
if (val) out.avoid = val.split(/[、,;\s]+/).map((s) => s.trim()).filter(Boolean)
} else {
out[key] = val
}
}
return out
}
/** @deprecated 使用 parsePartialDietPlainText */
export function parsePartialDietJson(text) {
return parsePartialDietPlainText(text)
}
/**
* SSE 流式请求(微信小程序 enableChunked;其它端走 fallback
*
* @param {object} opts
* @param {string} opts.baseUrl
* @param {string} opts.url
* @param {string} [opts.method]
* @param {object} [opts.data]
* @param {function(string, object):void} opts.onEvent
* @param {function():Promise<object>} [opts.fallback] 不支持流式时的降级请求
*/
export function requestAiStream(opts) {
const {
baseUrl,
url,
method = 'GET',
data = {},
onEvent,
fallback
} = opts
// #ifdef MP-WEIXIN
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('token') || ''
let sseBuffer = ''
let requestUrl = joinApiUrl(baseUrl, url)
if (method === 'GET') {
requestUrl = appendQuery(requestUrl, data)
}
const task = wx.request({
url: requestUrl,
method,
data: method === 'POST' ? data : {},
enableChunked: true,
timeout: 60000,
header: {
token,
'content-type': 'application/x-www-form-urlencoded',
'Cache-Control': 'no-cache'
},
success: (res) => {
if (res && res.data) {
sseBuffer += decodeChunkData(res.data)
sseBuffer = consumeSseBuffer(sseBuffer, onEvent)
}
resolve(res)
},
fail: (err) => reject(err)
})
if (task && typeof task.onChunkReceived === 'function') {
task.onChunkReceived((res) => {
sseBuffer += decodeChunkData(res.data)
sseBuffer = consumeSseBuffer(sseBuffer, onEvent)
})
} else if (typeof fallback === 'function') {
fallback().then(resolve).catch(reject)
} else {
reject(new Error('当前环境不支持流式请求'))
}
})
// #endif
// #ifndef MP-WEIXIN
if (typeof fallback === 'function') {
return fallback()
}
return Promise.reject(new Error('当前环境不支持流式请求'))
// #endif
}