From 5f4d084e63e77b8316895da0718833e6bc7aff4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 07:37:52 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=87=8D=E5=86=99=20streamApiKeyReply?= =?UTF-8?q?=20=E4=B8=BA=20SSE=20=E6=B5=81=E5=BC=8F=E8=B0=83=E7=94=A8?= =?UTF-8?q?=EF=BC=8C=E4=B8=8E=20docs/index.html=20=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对比发现 docs/index.html 使用 stream:true + getReader() SSE 流式 读取模型回复(已验证可用),而 persona-studio 使用非流式 JSON 请求 导致模型无法回复。 改动: - 发送请求时加入 stream: true 参数 - 使用 getReader() + TextDecoder 逐行读取 SSE 响应 - 解析 data: 行提取 delta.content 增量内容 - 实时更新对话气泡(逐字显示+光标) - CORS 失败时降级到后端代理(非流式) Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> --- .../brain/memory/EXP-001/memory.json | 28 ++++++ persona-studio/frontend/chat.js | 97 ++++++++++++------- 2 files changed, 90 insertions(+), 35 deletions(-) create mode 100644 persona-studio/brain/memory/EXP-001/memory.json diff --git a/persona-studio/brain/memory/EXP-001/memory.json b/persona-studio/brain/memory/EXP-001/memory.json new file mode 100644 index 00000000..fd0d7c16 --- /dev/null +++ b/persona-studio/brain/memory/EXP-001/memory.json @@ -0,0 +1,28 @@ +{ + "dev_id": "EXP-001", + "conversations": [ + { + "role": "user", + "content": "你好", + "timestamp": "2026-03-11T07:33:21.330Z" + }, + { + "role": "assistant", + "content": "你好!我是知秋。告诉我你想做什么,我们一起聊聊方案 😊", + "timestamp": "2026-03-11T07:33:21.331Z" + }, + { + "role": "user", + "content": "测试消息随便写", + "timestamp": "2026-03-11T07:33:21.340Z" + }, + { + "role": "assistant", + "content": "好的,让我了解一下你的需求:\n\n1. 你想做什么类型的项目?(网站 / 工具 / 组件 / 其他)\n2. 有哪些核心功能?\n3. 有没有参考设计?\n\n跟我聊聊,我帮你理清思路 💙", + "timestamp": "2026-03-11T07:33:21.340Z" + } + ], + "last_topic": "测试消息随便写", + "preferences": {}, + "updated_at": "2026-03-11T07:33:21.341Z" +} \ No newline at end of file diff --git a/persona-studio/frontend/chat.js b/persona-studio/frontend/chat.js index d6f696a8..bba8d876 100644 --- a/persona-studio/frontend/chat.js +++ b/persona-studio/frontend/chat.js @@ -170,48 +170,81 @@ async function sendMessage() { input.focus(); } -/* ---- API Key 对话(浏览器直连用户 API,后端代理作为降级) ---- */ +/* ---- API Key 对话(浏览器直连 SSE 流式,与 docs/index.html 相同方式) ---- */ async function streamApiKeyReply(text) { var apiMessages = [ZHIQIU_SYSTEM_PROMPT].concat(conversationHistory.slice(-20).map(function (msg) { return { role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.content }; })); var streamEl = appendStreamMessage(); - var requestBody = JSON.stringify({ + var directUrl = USER_API_BASE.replace(/\/+$/, '') + '/chat/completions'; + var reqBody = { model: SELECTED_MODEL, messages: apiMessages, + stream: true, max_tokens: 2000, temperature: 0.8 - }); + }; - var reply = null; - - // 策略 1:浏览器直连用户 API(与模型检测相同路径,最可靠) try { - var directUrl = USER_API_BASE.replace(/\/+$/, '') + '/chat/completions'; - var directRes = await fetch(directUrl, { + var res = await fetch(directUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + USER_API_KEY }, - body: requestBody + body: JSON.stringify(reqBody) }); - if (directRes.ok) { - var directData = await directRes.json(); - if (directData.choices && directData.choices[0] && directData.choices[0].message) { - reply = directData.choices[0].message.content; + if (!res.ok) { + var errText = '请求失败 (HTTP ' + res.status + ')'; + try { + var errData = await res.json(); + errText = (errData.error && errData.error.message) || errData.message || errText; + } catch (_e) { /* ignore parse error */ } + streamEl.textContent = '⚠️ ' + errText; + return; + } + + // SSE 流式读取(与 docs/index.html streamReply 相同逻辑) + var full = ''; + var reader = res.body.getReader(); + var decoder = new TextDecoder(); + var buf = ''; + + while (true) { + var chunk = await reader.read(); + if (chunk.done) break; + buf += decoder.decode(chunk.value, { stream: true }); + var lines = buf.split('\n'); + buf = lines.pop(); + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + if (!line.startsWith('data: ')) continue; + var d = line.slice(6); + if (d === '[DONE]') continue; + try { + var parsed = JSON.parse(d); + var delta = parsed.choices && parsed.choices[0] && parsed.choices[0].delta; + var content = delta && delta.content; + if (content) { + full += content; + streamEl.textContent = full + '▋'; + var chatBody = document.getElementById('chatBody'); + chatBody.scrollTop = chatBody.scrollHeight; + } + } catch (_parseErr) { /* ignore malformed SSE line */ } } } - } catch (_directErr) { - // 浏览器直连失败(CORS 或网络),降级到后端代理 - } - // 策略 2:后端代理(处理 CORS 限制的情况) - if (!reply) { + streamEl.textContent = full || '(未收到有效回复)'; + if (full) { + conversationHistory.push({ role: 'assistant', content: full }); + } + } catch (err) { + // 浏览器直连失败(CORS 或网络),降级到后端代理 try { - var res = await fetch(API_BASE + '/api/ps/apikey/chat', { + var proxyRes = await fetch(API_BASE + '/api/ps/apikey/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -222,32 +255,26 @@ async function streamApiKeyReply(text) { }) }); - if (res.ok) { - var data = await res.json(); + if (proxyRes.ok) { + var data = await proxyRes.json(); if (data.reply) { - reply = data.reply; + streamEl.textContent = data.reply; + conversationHistory.push({ role: 'assistant', content: data.reply }); + } else { + streamEl.textContent = '(未收到有效回复)'; } } else { - var errText = '请求失败 (HTTP ' + res.status + ')'; + var proxyErrText = '请求失败 (HTTP ' + proxyRes.status + ')'; try { - var errData = await res.json(); - errText = errData.message || errText; + var proxyErrData = await proxyRes.json(); + proxyErrText = proxyErrData.message || proxyErrText; } catch (_e) { /* ignore */ } - streamEl.textContent = '⚠️ ' + errText; - return; + streamEl.textContent = '⚠️ ' + proxyErrText; } } catch (proxyErr) { streamEl.textContent = '⚠️ ' + (proxyErr.message || '请求失败,请检查网络连接'); - return; } } - - if (reply) { - streamEl.textContent = reply; - conversationHistory.push({ role: 'assistant', content: reply }); - } else { - streamEl.textContent = '(未收到有效回复)'; - } } /* ---- 思考中状态 ---- */