fix: 重写 streamApiKeyReply 为 SSE 流式调用,与 docs/index.html 一致
对比发现 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>
This commit is contained in:
parent
4ad3636098
commit
5f4d084e63
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -170,48 +170,81 @@ async function sendMessage() {
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- API Key 对话(浏览器直连用户 API,后端代理作为降级) ---- */
|
/* ---- API Key 对话(浏览器直连 SSE 流式,与 docs/index.html 相同方式) ---- */
|
||||||
async function streamApiKeyReply(text) {
|
async function streamApiKeyReply(text) {
|
||||||
var apiMessages = [ZHIQIU_SYSTEM_PROMPT].concat(conversationHistory.slice(-20).map(function (msg) {
|
var apiMessages = [ZHIQIU_SYSTEM_PROMPT].concat(conversationHistory.slice(-20).map(function (msg) {
|
||||||
return { role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.content };
|
return { role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.content };
|
||||||
}));
|
}));
|
||||||
|
|
||||||
var streamEl = appendStreamMessage();
|
var streamEl = appendStreamMessage();
|
||||||
var requestBody = JSON.stringify({
|
var directUrl = USER_API_BASE.replace(/\/+$/, '') + '/chat/completions';
|
||||||
|
var reqBody = {
|
||||||
model: SELECTED_MODEL,
|
model: SELECTED_MODEL,
|
||||||
messages: apiMessages,
|
messages: apiMessages,
|
||||||
|
stream: true,
|
||||||
max_tokens: 2000,
|
max_tokens: 2000,
|
||||||
temperature: 0.8
|
temperature: 0.8
|
||||||
});
|
};
|
||||||
|
|
||||||
var reply = null;
|
|
||||||
|
|
||||||
// 策略 1:浏览器直连用户 API(与模型检测相同路径,最可靠)
|
|
||||||
try {
|
try {
|
||||||
var directUrl = USER_API_BASE.replace(/\/+$/, '') + '/chat/completions';
|
var res = await fetch(directUrl, {
|
||||||
var directRes = await fetch(directUrl, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Bearer ' + USER_API_KEY
|
'Authorization': 'Bearer ' + USER_API_KEY
|
||||||
},
|
},
|
||||||
body: requestBody
|
body: JSON.stringify(reqBody)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (directRes.ok) {
|
if (!res.ok) {
|
||||||
var directData = await directRes.json();
|
var errText = '请求失败 (HTTP ' + res.status + ')';
|
||||||
if (directData.choices && directData.choices[0] && directData.choices[0].message) {
|
try {
|
||||||
reply = directData.choices[0].message.content;
|
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 限制的情况)
|
streamEl.textContent = full || '(未收到有效回复)';
|
||||||
if (!reply) {
|
if (full) {
|
||||||
|
conversationHistory.push({ role: 'assistant', content: full });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 浏览器直连失败(CORS 或网络),降级到后端代理
|
||||||
try {
|
try {
|
||||||
var res = await fetch(API_BASE + '/api/ps/apikey/chat', {
|
var proxyRes = await fetch(API_BASE + '/api/ps/apikey/chat', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
@ -222,32 +255,26 @@ async function streamApiKeyReply(text) {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (proxyRes.ok) {
|
||||||
var data = await res.json();
|
var data = await proxyRes.json();
|
||||||
if (data.reply) {
|
if (data.reply) {
|
||||||
reply = data.reply;
|
streamEl.textContent = data.reply;
|
||||||
|
conversationHistory.push({ role: 'assistant', content: data.reply });
|
||||||
|
} else {
|
||||||
|
streamEl.textContent = '(未收到有效回复)';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var errText = '请求失败 (HTTP ' + res.status + ')';
|
var proxyErrText = '请求失败 (HTTP ' + proxyRes.status + ')';
|
||||||
try {
|
try {
|
||||||
var errData = await res.json();
|
var proxyErrData = await proxyRes.json();
|
||||||
errText = errData.message || errText;
|
proxyErrText = proxyErrData.message || proxyErrText;
|
||||||
} catch (_e) { /* ignore */ }
|
} catch (_e) { /* ignore */ }
|
||||||
streamEl.textContent = '⚠️ ' + errText;
|
streamEl.textContent = '⚠️ ' + proxyErrText;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} catch (proxyErr) {
|
} catch (proxyErr) {
|
||||||
streamEl.textContent = '⚠️ ' + (proxyErr.message || '请求失败,请检查网络连接');
|
streamEl.textContent = '⚠️ ' + (proxyErr.message || '请求失败,请检查网络连接');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reply) {
|
|
||||||
streamEl.textContent = reply;
|
|
||||||
conversationHistory.push({ role: 'assistant', content: reply });
|
|
||||||
} else {
|
|
||||||
streamEl.textContent = '(未收到有效回复)';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 思考中状态 ---- */
|
/* ---- 思考中状态 ---- */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue