* 流式发送消息(SSE),支持附件。 * @param {FormData} formData 包含 message, user_id, session_id, files 等 * @param {function} onToken 每收到一个 token 时回调 (tokenString) * @param {function} onDone 完成时回调 ({session_id, emotion, suggestions}) * @param {function} onError 出错时回调 (errorString) * @returns
(formData, { onToken, onDone, onError })
| 41 | * @returns {function} abort 调用可取消请求 |
| 42 | */ |
| 43 | static sendMessageStreaming(formData, { onToken, onDone, onError }) { |
| 44 | const controller = new AbortController(); |
| 45 | |
| 46 | (async () => { |
| 47 | try { |
| 48 | const response = await fetch(`${API_BASE_URL}/chat/stream`, { |
| 49 | method: 'POST', |
| 50 | body: formData, |
| 51 | signal: controller.signal, |
| 52 | }); |
| 53 | |
| 54 | if (!response.ok) { |
| 55 | const errText = await response.text(); |
| 56 | onError && onError(`服务器错误 (${response.status}): ${errText}`); |
| 57 | return; |
| 58 | } |
| 59 | |
| 60 | const reader = response.body.getReader(); |
| 61 | const decoder = new TextDecoder(); |
| 62 | let buffer = ''; |
| 63 | |
| 64 | while (true) { |
| 65 | const { done, value } = await reader.read(); |
| 66 | if (done) break; |
| 67 | |
| 68 | buffer += decoder.decode(value, { stream: true }); |
| 69 | const lines = buffer.split('\n'); |
| 70 | buffer = lines.pop(); // keep incomplete line |
| 71 | |
| 72 | for (const line of lines) { |
| 73 | if (!line.startsWith('data: ')) continue; |
| 74 | const payload = line.slice(6).trim(); |
| 75 | if (payload === '[DONE]') continue; |
| 76 | try { |
| 77 | const data = JSON.parse(payload); |
| 78 | if (data.type === 'token' && data.content) { |
| 79 | onToken && onToken(data.content); |
| 80 | } else if (data.type === 'done') { |
| 81 | onDone && onDone(data); |
| 82 | } else if (data.type === 'error') { |
| 83 | onError && onError(data.content || '未知错误'); |
| 84 | } |
| 85 | } catch { |
| 86 | // ignore parse errors for partial data |
| 87 | } |
| 88 | } |
| 89 | } |
| 90 | } catch (err) { |
| 91 | if (err.name !== 'AbortError') { |
| 92 | onError && onError(err.message || '网络错误'); |
| 93 | } |
| 94 | } |
| 95 | })(); |
| 96 | |
| 97 | return () => controller.abort(); |
| 98 | } |
| 99 | |
| 100 | static async submitFeedback(feedbackData) { |