✨ 新增 Vercel 跨域代理支持和环境检测功能
- 在核心服务中添加 Vercel 环境检测和代理工具函数 - 更新 LLM 服务,支持通过 Vercel 代理解决跨域问题 - 扩展模型配置类型,新增 `useVercelProxy` 选项 - 在 ModelManager 组件中添加 Vercel 代理可用性检测和配置 - 更新技术开发指南,详细说明跨域代理解决方案 - 调整 Vercel 配置,支持 API 代理和环境变量设置
This commit is contained in:
1 parent
964a8676fc
commit
bf9d98eb69
13 files changed
+553
-59
No files matched your search
@@ -0,0 +1,95 @@
|
||||
export const config = {
|
||||
runtime: 'edge'
|
||||
};
|
||||
|
||||
export default async function handler(req) {
|
||||
// 处理CORS预检请求
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-KEY',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析请求数据
|
||||
const { searchParams } = new URL(req.url);
|
||||
const targetUrl = searchParams.get('targetUrl');
|
||||
|
||||
if (!targetUrl) {
|
||||
return new Response(JSON.stringify({ error: '缺少目标URL参数' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 确保targetUrl是有效的URL
|
||||
let validTargetUrl;
|
||||
try {
|
||||
validTargetUrl = new URL(decodeURIComponent(targetUrl)).toString();
|
||||
console.log('目标URL:', validTargetUrl);
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: `无效的目标URL: ${error.message}` }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 准备请求头
|
||||
const headers = new Headers();
|
||||
req.headers.forEach((value, key) => {
|
||||
// 排除一些特定的头,这些头可能会导致问题
|
||||
if (!['host', 'connection', 'content-length'].includes(key.toLowerCase())) {
|
||||
headers.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取请求体
|
||||
let body = null;
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
||||
body = await req.text();
|
||||
}
|
||||
|
||||
// 发送请求到目标URL
|
||||
const fetchResponse = await fetch(validTargetUrl, {
|
||||
method: req.method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
// 读取响应数据
|
||||
const data = await fetchResponse.text();
|
||||
|
||||
// 创建响应头
|
||||
const responseHeaders = new Headers();
|
||||
fetchResponse.headers.forEach((value, key) => {
|
||||
responseHeaders.set(key, value);
|
||||
});
|
||||
|
||||
// 设置CORS头
|
||||
responseHeaders.set('Access-Control-Allow-Origin', '*');
|
||||
responseHeaders.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
responseHeaders.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-KEY');
|
||||
|
||||
// 返回响应
|
||||
return new Response(data, {
|
||||
status: fetchResponse.status,
|
||||
statusText: fetchResponse.statusText,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('代理请求失败:', error);
|
||||
return new Response(JSON.stringify({ error: `代理请求失败: ${error.message}` }), {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
// api/stream.js
|
||||
export const config = {
|
||||
runtime: 'edge'
|
||||
};
|
||||
|
||||
export default async function handler(req) {
|
||||
console.log('流式代理请求开始处理:', new Date().toISOString());
|
||||
|
||||
// 处理CORS预检请求
|
||||
if (req.method === 'OPTIONS') {
|
||||
console.log('处理CORS预检请求');
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-KEY',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析请求数据
|
||||
const { searchParams } = new URL(req.url);
|
||||
const targetUrl = searchParams.get('targetUrl');
|
||||
|
||||
if (!targetUrl) {
|
||||
console.error('缺少目标URL参数');
|
||||
return new Response(JSON.stringify({ error: '缺少目标URL参数' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 确保targetUrl是有效的URL
|
||||
let validTargetUrl;
|
||||
try {
|
||||
validTargetUrl = new URL(decodeURIComponent(targetUrl)).toString();
|
||||
console.log('目标URL:', validTargetUrl);
|
||||
} catch (error) {
|
||||
console.error('无效的目标URL:', error);
|
||||
return new Response(JSON.stringify({ error: `无效的目标URL: ${error.message}` }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 准备请求头
|
||||
const headers = new Headers();
|
||||
req.headers.forEach((value, key) => {
|
||||
// 排除一些特定的头,这些头可能会导致问题
|
||||
if (!['host', 'connection', 'content-length'].includes(key.toLowerCase())) {
|
||||
headers.set(key, value);
|
||||
}
|
||||
});
|
||||
console.log('请求方法:', req.method);
|
||||
console.log('请求头数量:', [...headers.keys()].length);
|
||||
|
||||
// 获取请求体
|
||||
let body = null;
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
||||
body = await req.text();
|
||||
console.log('请求体长度:', body?.length || 0);
|
||||
}
|
||||
|
||||
console.log('开始向目标URL发送请求:', new Date().toISOString());
|
||||
// 发送请求到目标URL
|
||||
const fetchResponse = await fetch(validTargetUrl, {
|
||||
method: req.method,
|
||||
headers,
|
||||
body,
|
||||
duplex: 'half', // 支持流式请求
|
||||
});
|
||||
console.log('收到目标URL响应:', new Date().toISOString(), '状态码:', fetchResponse.status);
|
||||
|
||||
// 创建响应头
|
||||
const responseHeaders = new Headers();
|
||||
fetchResponse.headers.forEach((value, key) => {
|
||||
responseHeaders.set(key, value);
|
||||
});
|
||||
|
||||
// 设置CORS头
|
||||
responseHeaders.set('Access-Control-Allow-Origin', '*');
|
||||
responseHeaders.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
responseHeaders.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-KEY');
|
||||
|
||||
// 检查是否是SSE流
|
||||
const contentType = fetchResponse.headers.get('content-type');
|
||||
const isEventStream = contentType?.includes('text/event-stream');
|
||||
console.log('响应内容类型:', contentType, '是否为SSE流:', isEventStream);
|
||||
|
||||
if (isEventStream) {
|
||||
responseHeaders.set('Content-Type', 'text/event-stream');
|
||||
responseHeaders.set('Cache-Control', 'no-cache');
|
||||
responseHeaders.set('Connection', 'keep-alive');
|
||||
// 确保不缓冲数据
|
||||
responseHeaders.set('X-Accel-Buffering', 'no');
|
||||
}
|
||||
|
||||
// 创建并返回流式响应,使用TransformStream确保数据立即传输
|
||||
const { readable, writable } = new TransformStream();
|
||||
console.log('创建TransformStream完成');
|
||||
|
||||
// 启动数据传输过程
|
||||
(async () => {
|
||||
const writer = writable.getWriter();
|
||||
const reader = fetchResponse.body.getReader();
|
||||
|
||||
try {
|
||||
console.log('开始流式传输数据:', new Date().toISOString());
|
||||
let chunkCount = 0;
|
||||
let totalBytes = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
console.log('流式传输完成:', new Date().toISOString());
|
||||
console.log(`总共传输 ${chunkCount} 个数据块,${totalBytes} 字节`);
|
||||
await writer.close();
|
||||
break;
|
||||
}
|
||||
|
||||
// 立即写入数据并刷新
|
||||
await writer.write(value);
|
||||
chunkCount++;
|
||||
totalBytes += value.length;
|
||||
|
||||
if (chunkCount % 10 === 0) {
|
||||
console.log(`已传输 ${chunkCount} 个数据块,${totalBytes} 字节`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('流式传输错误:', error);
|
||||
writer.abort(error);
|
||||
}
|
||||
})();
|
||||
|
||||
console.log('返回流式响应');
|
||||
return new Response(readable, {
|
||||
status: fetchResponse.status,
|
||||
statusText: fetchResponse.statusText,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('流式代理请求失败:', error);
|
||||
return new Response(JSON.stringify({ error: `流式代理请求失败: ${error.message}` }), {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export const config = {
|
||||
runtime: 'edge'
|
||||
};
|
||||
|
||||
export default async function handler(request) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
status: 'available',
|
||||
environment: 'vercel',
|
||||
proxySupport: true,
|
||||
version: '1.0.0'
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user