const app = getApp() function apiUrl(path) { const base = (app && app.globalData && app.globalData.apiBase) || 'http://localhost:8000' return base.replace(/\/$/, '') + path } function authHeader() { const token = (app && app.globalData && app.globalData.token) || wx.getStorageSync('token') return token ? { Authorization: 'Bearer ' + token } : {} } function request(path, options) { options = options || {} return new Promise((resolve, reject) => { wx.request({ url: apiUrl(path), method: options.method || 'GET', data: options.data, header: Object.assign( { 'Content-Type': 'application/json' }, authHeader(), options.header || {} ), success(res) { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data) } else { const detail = (res.data && (res.data.detail || res.data.message)) || res.errMsg || '请求失败' reject(new Error(typeof detail === 'string' ? detail : JSON.stringify(detail))) } }, fail(err) { reject(new Error(err.errMsg || '网络异常')) } }) }) } function uploadFile(path, filePath, name) { return new Promise((resolve, reject) => { wx.uploadFile({ url: apiUrl(path), filePath: filePath, name: name || 'file', header: authHeader(), success(res) { if (res.statusCode >= 200 && res.statusCode < 300) { try { resolve(JSON.parse(res.data)) } catch (e) { resolve(res.data) } } else { reject(new Error(res.data || '上传失败')) } }, fail(err) { reject(new Error(err.errMsg || '上传失败')) } }) }) } /** * 以分块方式请求 SSE 流。onEvent({event, data}) 回调每个事件。 * 返回一个 requestTask,可用于中断。 */ function streamRequest(path, body, onEvent, onDone, onError) { const task = wx.request({ url: apiUrl(path), method: 'POST', data: body, responseType: 'text', enableChunked: true, header: Object.assign( { 'Content-Type': 'application/json', Accept: 'text/event-stream' }, authHeader() ), success(res) { if (res.statusCode < 200 || res.statusCode >= 300) { onError && onError(new Error('HTTP ' + res.statusCode + ' ' + (res.data || ''))) } else { onDone && onDone() } }, fail(err) { onError && onError(new Error(err.errMsg || '流式请求失败')) } }) let buf = '' const parseChunk = (chunk) => { buf += chunk // SSE 事件以空行分隔(\n\n) let idx while ((idx = buf.indexOf('\n\n')) !== -1) { const raw = buf.slice(0, idx) buf = buf.slice(idx + 2) let event = 'message' let dataStr = '' raw.split('\n').forEach((line) => { if (line.startsWith('event:')) event = line.slice(6).trim() else if (line.startsWith('data:')) dataStr += line.slice(5).trim() }) let data try { data = JSON.parse(dataStr) } catch (e) { data = dataStr } onEvent && onEvent({ event: event, data: data }) } } if (task && task.onChunkReceived) { task.onChunkReceived((response) => { const buffer = response.data let text = '' if (typeof buffer === 'string') { text = buffer } else { // ArrayBuffer -> UTF-8 const bytes = new Uint8Array(buffer) // 简易 UTF-8 解码 try { if (typeof TextDecoder !== 'undefined') { text = new TextDecoder('utf-8').decode(bytes) } else { text = decodeURIComponent(escape(String.fromCharCode.apply(null, bytes))) } } catch (e) { text = String.fromCharCode.apply(null, bytes) } } parseChunk(text) }) } return task } module.exports = { apiUrl: apiUrl, request: request, uploadFile: uploadFile, streamRequest: streamRequest }