Files
NekoAI/src/index.js
2026-03-13 17:00:59 +08:00

133 lines
3.4 KiB
JavaScript

export default {
async fetch(request, env) {
const url = new URL(request.url);
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders(request, env) });
}
if (url.pathname === '/api/models' && request.method === 'GET') {
return handleModels(request, env);
}
if (url.pathname === '/api/chat' && request.method === 'POST') {
return handleChat(request, env);
}
return json({ error: 'Not found' }, 404, request, env);
},
};
async function handleModels(request, env) {
const authError = verifyAccessKey(request, env);
if (authError) return authError;
const upstream = await fetch(joinUrl(env.OPENAI_BASE_URL, '/v1/models'), {
method: 'GET',
headers: {
Authorization: `Bearer ${env.OPENAI_API_KEY}`,
},
});
const text = await upstream.text();
let data;
try {
data = JSON.parse(text);
} catch {
return json({ error: 'Invalid upstream /v1/models response', raw: text.slice(0, 500) }, 502, request, env);
}
if (!upstream.ok) {
return json(
{
error: data?.error?.message || data?.error || `Upstream /v1/models failed with HTTP ${upstream.status}`,
upstreamStatus: upstream.status,
},
upstream.status,
request,
env,
);
}
const models = Array.isArray(data?.data)
? data.data
.filter((item) => item && typeof item.id === 'string' && item.id.trim())
.map((item) => ({ id: item.id, label: item.id }))
: [];
return json({ models }, 200, request, env);
}
async function handleChat(request, env) {
const authError = verifyAccessKey(request, env);
if (authError) return authError;
let payload;
try {
payload = await request.json();
} catch {
return json({ error: 'Invalid JSON body' }, 400, request, env);
}
const upstream = await fetch(joinUrl(env.OPENAI_BASE_URL, '/v1/chat/completions'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${env.OPENAI_API_KEY}`,
},
body: JSON.stringify(payload),
});
const headers = new Headers(corsHeaders(request, env));
const contentType = upstream.headers.get('content-type');
if (contentType) headers.set('content-type', contentType);
return new Response(upstream.body, {
status: upstream.status,
headers,
});
}
function verifyAccessKey(request, env) {
const auth = request.headers.get('authorization') || '';
const expected = `Bearer ${env.ACCESS_KEY}`;
if (!env.ACCESS_KEY) {
return json({ error: 'Server ACCESS_KEY is not configured' }, 500, request, env);
}
if (auth !== expected) {
return json({ error: 'Unauthorized' }, 401, request, env);
}
return null;
}
function joinUrl(base, path) {
return `${String(base || '').replace(/\/$/, '')}${path}`;
}
function corsHeaders(request, env) {
const origin = request?.headers?.get('origin');
const allowedOrigin = env.ALLOWED_ORIGIN || '*';
const finalOrigin = allowedOrigin === '*' ? '*' : origin || allowedOrigin;
return {
'Access-Control-Allow-Origin': finalOrigin,
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Vary': 'Origin',
};
}
function json(data, status = 200, request, env) {
return new Response(JSON.stringify(data), {
status,
headers: {
'Content-Type': 'application/json; charset=utf-8',
...corsHeaders(request, env),
},
});
}