fix: 修复模型无法回复用户消息 — 浏览器直连 API + 后端 URL 构造修复

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-03-11 03:04:29 +00:00
parent 91a08606a2
commit ff3ec0d024
3 changed files with 69 additions and 59 deletions

View File

@ -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,

View File

@ -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"
}

View File

@ -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 || '请求失败,请检查网络连接');
} }
} }