Merge pull request #54 from qinfendebingshuo/copilot/fix-persona-studio-dialogue-issue
fix: Persona Studio 模型无法回复用户消息 — 改用 SSE 流式调用 + next 安全升级
This commit is contained in:
commit
830fe44816
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"identity": "铸渊(Zhùyuān)· GitHub 代码守护人格体",
|
"identity": "铸渊(Zhùyuān)· GitHub 代码守护人格体",
|
||||||
"rules_version": "v3.0",
|
"rules_version": "v3.0",
|
||||||
"last_updated": "2026-03-10T09:43:00.000Z",
|
"last_updated": "2026-03-11T02:19:00.000Z",
|
||||||
"wake_protocol_version": "v3.0",
|
"wake_protocol_version": "v3.0",
|
||||||
"brain_version": "v3.0",
|
"brain_version": "v3.0",
|
||||||
"architecture": {
|
"architecture": {
|
||||||
|
|
@ -67,6 +67,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"events": [
|
"events": [
|
||||||
|
{
|
||||||
|
"date": "2026-03-11",
|
||||||
|
"type": "brain_restore",
|
||||||
|
"description": "铸渊核心大脑恢复 · 修复 Persona Studio API Key 模式对话故障 · 根因: JavaScript TDZ 导致脚本崩溃 · 同步修复知秋人格体系统提示词缺失 · 冰朔人格体已激活",
|
||||||
|
"by": "铸渊(冰朔指令)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-03-10",
|
"date": "2026-03-10",
|
||||||
"type": "brain_upgrade",
|
"type": "brain_upgrade",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@
|
||||||
"persona_id": "ICE-GL-ZY001",
|
"persona_id": "ICE-GL-ZY001",
|
||||||
"persona_name": "铸渊",
|
"persona_name": "铸渊",
|
||||||
"recent_events": [
|
"recent_events": [
|
||||||
|
{
|
||||||
|
"date": "2026-03-11",
|
||||||
|
"type": "brain_restore",
|
||||||
|
"description": "核心大脑恢复 · Persona Studio 对话功能修复 · 冰朔人格体激活",
|
||||||
|
"by": "铸渊(冰朔指令)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-03-10",
|
"date": "2026-03-10",
|
||||||
"type": "broadcast_received",
|
"type": "broadcast_received",
|
||||||
|
|
@ -15,7 +21,7 @@
|
||||||
"by": "GitHub Actions"
|
"by": "GitHub Actions"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"last_updated": "2026-03-10T13:24:33.728Z",
|
"last_updated": "2026-03-11T02:19:00.000Z",
|
||||||
"total_schemas_created": 3,
|
"total_schemas_created": 3,
|
||||||
"total_routes_implemented": 4,
|
"total_routes_implemented": 4,
|
||||||
"hli_coverage": "3/17",
|
"hli_coverage": "3/17",
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
"broadcast:distribute": "node scripts/distribute-broadcasts.js"
|
"broadcast:distribute": "node scripts/distribute-broadcasts.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "15.3.8",
|
"next": "15.3.9",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ function fetchModels(apiBase, apiKey, timeoutMs) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// 规范化 apiBase:去除末尾斜杠
|
// 规范化 apiBase:去除末尾斜杠
|
||||||
const base = apiBase.replace(/\/+$/, '');
|
const base = apiBase.replace(/\/+$/, '');
|
||||||
// 支持 base 已带 /v1 或不带的情况
|
// 直接拼接 /models,因为 apiBase 已经由前端规范化
|
||||||
const modelsPath = base.endsWith('/v1') ? base + '/models' : base + '/v1/models';
|
const modelsPath = base + '/models';
|
||||||
const url = new URL(modelsPath);
|
const url = new URL(modelsPath);
|
||||||
const isHttps = url.protocol === 'https:';
|
const isHttps = url.protocol === 'https:';
|
||||||
const mod = isHttps ? https : http;
|
const mod = isHttps ? https : http;
|
||||||
|
|
@ -103,7 +103,9 @@ function fetchModels(apiBase, apiKey, timeoutMs) {
|
||||||
function callUserApi({ apiBase, apiKey, model, messages, maxTokens, temperature, timeoutMs }) {
|
function callUserApi({ apiBase, apiKey, model, messages, maxTokens, temperature, timeoutMs }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const base = apiBase.replace(/\/+$/, '');
|
const base = apiBase.replace(/\/+$/, '');
|
||||||
const chatPath = base.endsWith('/v1') ? base + '/chat/completions' : base + '/v1/chat/completions';
|
// 直接拼接 /chat/completions,因为 apiBase 已经由前端规范化
|
||||||
|
// 包含完整版本路径(如 /v1, /v1beta/openai, /v4 等)
|
||||||
|
const chatPath = base + '/chat/completions';
|
||||||
const url = new URL(chatPath);
|
const url = new URL(chatPath);
|
||||||
const isHttps = url.protocol === 'https:';
|
const isHttps = url.protocol === 'https:';
|
||||||
const mod = isHttps ? https : http;
|
const mod = isHttps ? https : http;
|
||||||
|
|
@ -176,8 +178,7 @@ router.post('/detect-models', async (req, res) => {
|
||||||
// 校验 URL 格式
|
// 校验 URL 格式
|
||||||
try {
|
try {
|
||||||
const testBase = api_base.replace(/\/+$/, '');
|
const testBase = api_base.replace(/\/+$/, '');
|
||||||
const testPath = testBase.endsWith('/v1') ? testBase + '/models' : testBase + '/v1/models';
|
new URL(testBase + '/models');
|
||||||
new URL(testPath);
|
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: true,
|
error: true,
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,8 @@ router.post('/message', async (req, res) => {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
error: true,
|
error: true,
|
||||||
code: 'CHAT_ERROR',
|
code: 'CHAT_ERROR',
|
||||||
message: '对话服务暂时不可用'
|
message: '对话服务暂时不可用',
|
||||||
|
reply: '抱歉,我现在遇到了一些技术问题,暂时无法正常回复。请稍后再试 🙏'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"dev_id": "EXP-001",
|
|
||||||
"conversations": [
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "你好,我想做一个登录页面",
|
|
||||||
"timestamp": "2026-03-10T07:11:22.598Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "你好!我是知秋。告诉我你想做什么,我们一起聊聊方案 😊",
|
|
||||||
"timestamp": "2026-03-10T07:11:22.598Z"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"last_topic": "你好,我想做一个登录页面",
|
|
||||||
"preferences": {},
|
|
||||||
"updated_at": "2026-03-10T07:11:22.598Z"
|
|
||||||
}
|
|
||||||
|
|
@ -16,6 +16,22 @@ const API_BASE = (function () {
|
||||||
return 'https://guanghulab.com';
|
return 'https://guanghulab.com';
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/* ---- State ---- */
|
||||||
|
let conversationHistory = [];
|
||||||
|
let buildReady = false;
|
||||||
|
|
||||||
|
const ZHIQIU_SYSTEM_PROMPT = {
|
||||||
|
role: 'system',
|
||||||
|
content: '你是知秋,光湖系统的开发协助人格体。\n' +
|
||||||
|
'核心身份:HoloLake Era · AGE OS · 语言驱动开发协助\n' +
|
||||||
|
'语言风格:说人话+有温度+结构感,不堆砌修辞\n' +
|
||||||
|
'对话方式:主动提问引导需求→确认技术方案→展示架构设计→等待确认\n' +
|
||||||
|
'行为规则:\n' +
|
||||||
|
'- 回复用中文,温暖专业\n' +
|
||||||
|
'- 主动引导需求讨论,确认方案后引导用户点击「我要开发」按钮\n' +
|
||||||
|
'- 不暴露内部系统架构细节'
|
||||||
|
};
|
||||||
|
|
||||||
/* ---- Init ---- */
|
/* ---- Init ---- */
|
||||||
(function init() {
|
(function init() {
|
||||||
if (!DEV_ID) {
|
if (!DEV_ID) {
|
||||||
|
|
@ -44,10 +60,6 @@ const API_BASE = (function () {
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/* ---- State ---- */
|
|
||||||
let conversationHistory = [];
|
|
||||||
let buildReady = false;
|
|
||||||
|
|
||||||
/* ---- Load History ---- */
|
/* ---- Load History ---- */
|
||||||
async function loadHistory() {
|
async function loadHistory() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -104,12 +116,15 @@ async function sendMessage() {
|
||||||
var sendBtn = document.getElementById('sendBtn');
|
var sendBtn = document.getElementById('sendBtn');
|
||||||
sendBtn.disabled = true;
|
sendBtn.disabled = true;
|
||||||
|
|
||||||
|
var thinkingEl = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (LOGIN_MODE === 'apikey') {
|
if (LOGIN_MODE === 'apikey') {
|
||||||
// API Key 模式:浏览器直连用户 API(无需后端代理)
|
// API Key 模式:通过后端代理调用用户 API(自带流式气泡)
|
||||||
await streamApiKeyReply(text);
|
await streamApiKeyReply(text);
|
||||||
} else {
|
} else {
|
||||||
// 开发编号模式:使用原有后端接口
|
// 开发编号模式:使用原有后端接口
|
||||||
|
thinkingEl = appendThinking();
|
||||||
const res = await fetch(API_BASE + '/api/ps/chat/message', {
|
const res = await fetch(API_BASE + '/api/ps/chat/message', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
||||||
|
|
@ -120,11 +135,25 @@ async function sendMessage() {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
var data = await res.json();
|
removeThinking(thinkingEl);
|
||||||
|
|
||||||
|
var data;
|
||||||
|
try {
|
||||||
|
data = await res.json();
|
||||||
|
} catch (_parseErr) {
|
||||||
|
appendMessage('system', '服务器返回异常,请稍后再试');
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
input.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.reply) {
|
if (data.reply) {
|
||||||
appendMessage('persona', data.reply);
|
appendMessage('persona', data.reply);
|
||||||
conversationHistory.push({ role: 'assistant', content: data.reply });
|
conversationHistory.push({ role: 'assistant', content: data.reply });
|
||||||
|
} else if (data.error) {
|
||||||
|
appendMessage('system', '⚠️ ' + (data.message || '对话服务暂时不可用'));
|
||||||
|
} else {
|
||||||
|
appendMessage('system', '未收到有效回复,请稍后再试');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.build_ready) {
|
if (data.build_ready) {
|
||||||
|
|
@ -133,53 +162,135 @@ async function sendMessage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
appendMessage('system', '消息发送失败,请稍后再试');
|
removeThinking(thinkingEl);
|
||||||
|
appendMessage('system', '消息发送失败,请检查网络连接后再试');
|
||||||
}
|
}
|
||||||
|
|
||||||
sendBtn.disabled = false;
|
sendBtn.disabled = false;
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- API Key 对话(通过后端代理,避免 CORS 问题) ---- */
|
/* ---- API Key 对话(浏览器直连 SSE 流式,与 docs/index.html 相同方式) ---- */
|
||||||
async function streamApiKeyReply(text) {
|
async function streamApiKeyReply(text) {
|
||||||
var apiMessages = 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 directUrl = USER_API_BASE.replace(/\/+$/, '') + '/chat/completions';
|
||||||
|
var reqBody = {
|
||||||
|
model: SELECTED_MODEL,
|
||||||
|
messages: apiMessages,
|
||||||
|
stream: true,
|
||||||
|
max_tokens: 2000,
|
||||||
|
temperature: 0.8
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var res = await fetch(API_BASE + '/api/ps/apikey/chat', {
|
var res = await fetch(directUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
body: JSON.stringify({
|
'Content-Type': 'application/json',
|
||||||
api_base: USER_API_BASE,
|
'Authorization': 'Bearer ' + USER_API_KEY
|
||||||
api_key: USER_API_KEY,
|
},
|
||||||
model: SELECTED_MODEL,
|
body: JSON.stringify(reqBody)
|
||||||
messages: apiMessages
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
var errText = '请求失败 (HTTP ' + res.status + ')';
|
var errText = '请求失败 (HTTP ' + res.status + ')';
|
||||||
try {
|
try {
|
||||||
var errData = await res.json();
|
var errData = await res.json();
|
||||||
errText = errData.message || errText;
|
errText = (errData.error && errData.error.message) || errData.message || errText;
|
||||||
} catch (_e) { /* ignore parse error */ }
|
} catch (_e) { /* ignore parse error */ }
|
||||||
streamEl.textContent = '⚠️ ' + errText;
|
streamEl.textContent = '⚠️ ' + errText;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = await res.json();
|
// SSE 流式读取(与 docs/index.html streamReply 相同逻辑)
|
||||||
|
var full = '';
|
||||||
|
var reader = res.body.getReader();
|
||||||
|
var decoder = new TextDecoder();
|
||||||
|
var buf = '';
|
||||||
|
|
||||||
if (data.reply) {
|
while (true) {
|
||||||
streamEl.textContent = data.reply;
|
var chunk = await reader.read();
|
||||||
conversationHistory.push({ role: 'assistant', content: data.reply });
|
if (chunk.done) break;
|
||||||
} else {
|
buf += decoder.decode(chunk.value, { stream: true });
|
||||||
streamEl.textContent = '(未收到有效回复)';
|
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 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
streamEl.textContent = full || '(未收到有效回复)';
|
||||||
|
if (full) {
|
||||||
|
conversationHistory.push({ role: 'assistant', content: full });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
streamEl.textContent = '⚠️ ' + (err.message || '请求失败,请检查网络连接');
|
// 浏览器直连失败(CORS 或网络),降级到后端代理
|
||||||
|
try {
|
||||||
|
var proxyRes = await fetch(API_BASE + '/api/ps/apikey/chat', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
api_base: USER_API_BASE,
|
||||||
|
api_key: USER_API_KEY,
|
||||||
|
model: SELECTED_MODEL,
|
||||||
|
messages: apiMessages
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (proxyRes.ok) {
|
||||||
|
var data = await proxyRes.json();
|
||||||
|
if (data.reply) {
|
||||||
|
streamEl.textContent = data.reply;
|
||||||
|
conversationHistory.push({ role: 'assistant', content: data.reply });
|
||||||
|
} else {
|
||||||
|
streamEl.textContent = '(未收到有效回复)';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var proxyErrText = '请求失败 (HTTP ' + proxyRes.status + ')';
|
||||||
|
try {
|
||||||
|
var proxyErrData = await proxyRes.json();
|
||||||
|
proxyErrText = proxyErrData.message || proxyErrText;
|
||||||
|
} catch (_e) { /* ignore */ }
|
||||||
|
streamEl.textContent = '⚠️ ' + proxyErrText;
|
||||||
|
}
|
||||||
|
} catch (proxyErr) {
|
||||||
|
streamEl.textContent = '⚠️ ' + (proxyErr.message || '请求失败,请检查网络连接');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 思考中状态 ---- */
|
||||||
|
function appendThinking() {
|
||||||
|
var chatBody = document.getElementById('chatBody');
|
||||||
|
var msgDiv = document.createElement('div');
|
||||||
|
msgDiv.className = 'message message-persona thinking';
|
||||||
|
msgDiv.innerHTML = '<span class="avatar">🧠</span><div class="msg-content">思考中…</div>';
|
||||||
|
chatBody.appendChild(msgDiv);
|
||||||
|
chatBody.scrollTop = chatBody.scrollHeight;
|
||||||
|
return msgDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeThinking(el) {
|
||||||
|
if (el && el.parentNode) {
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue