fix: 修复模型无法回复用户消息 — 浏览器直连 API + 后端 URL 构造修复
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
parent
91a08606a2
commit
ff3ec0d024
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -1,28 +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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "你好",
|
|
||||||
"timestamp": "2026-03-11T03:01:16.220Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "你好!我是知秋。告诉我你想做什么,我们一起聊聊方案 😊",
|
|
||||||
"timestamp": "2026-03-11T03:01:16.220Z"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"last_topic": "你好",
|
|
||||||
"preferences": {},
|
|
||||||
"updated_at": "2026-03-11T03:01:16.220Z"
|
|
||||||
}
|
|
||||||
|
|
@ -170,46 +170,83 @@ async function sendMessage() {
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- API Key 对话(通过后端代理,避免 CORS 问题) ---- */
|
/* ---- API Key 对话(浏览器直连用户 API,后端代理作为降级) ---- */
|
||||||
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({
|
||||||
|
model: SELECTED_MODEL,
|
||||||
|
messages: apiMessages,
|
||||||
|
max_tokens: 2000,
|
||||||
|
temperature: 0.8
|
||||||
|
});
|
||||||
|
|
||||||
|
var reply = null;
|
||||||
|
|
||||||
|
// 策略 1:浏览器直连用户 API(与模型检测相同路径,最可靠)
|
||||||
try {
|
try {
|
||||||
var res = await fetch(API_BASE + '/api/ps/apikey/chat', {
|
var directUrl = USER_API_BASE.replace(/\/+$/, '') + '/chat/completions';
|
||||||
|
var directRes = 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: requestBody
|
||||||
messages: apiMessages
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (directRes.ok) {
|
||||||
var errText = '请求失败 (HTTP ' + res.status + ')';
|
var directData = await directRes.json();
|
||||||
try {
|
if (directData.choices && directData.choices[0] && directData.choices[0].message) {
|
||||||
var errData = await res.json();
|
reply = directData.choices[0].message.content;
|
||||||
errText = errData.message || errText;
|
}
|
||||||
} catch (_e) { /* ignore parse error */ }
|
}
|
||||||
streamEl.textContent = '⚠️ ' + errText;
|
} catch (_directErr) {
|
||||||
|
// 浏览器直连失败(CORS 或网络),降级到后端代理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 策略 2:后端代理(处理 CORS 限制的情况)
|
||||||
|
if (!reply) {
|
||||||
|
try {
|
||||||
|
var res = 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 (res.ok) {
|
||||||
|
var data = await res.json();
|
||||||
|
if (data.reply) {
|
||||||
|
reply = data.reply;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var errText = '请求失败 (HTTP ' + res.status + ')';
|
||||||
|
try {
|
||||||
|
var errData = await res.json();
|
||||||
|
errText = errData.message || errText;
|
||||||
|
} catch (_e) { /* ignore */ }
|
||||||
|
streamEl.textContent = '⚠️ ' + errText;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (proxyErr) {
|
||||||
|
streamEl.textContent = '⚠️ ' + (proxyErr.message || '请求失败,请检查网络连接');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var data = await res.json();
|
if (reply) {
|
||||||
|
streamEl.textContent = reply;
|
||||||
if (data.reply) {
|
conversationHistory.push({ role: 'assistant', content: reply });
|
||||||
streamEl.textContent = data.reply;
|
} else {
|
||||||
conversationHistory.push({ role: 'assistant', content: data.reply });
|
streamEl.textContent = '(未收到有效回复)';
|
||||||
} else {
|
|
||||||
streamEl.textContent = '(未收到有效回复)';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
streamEl.textContent = '⚠️ ' + (err.message || '请求失败,请检查网络连接');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue