241 lines
11 KiB
JavaScript
241 lines
11 KiB
JavaScript
import deepmerge from 'deepmerge';
|
||
import { handleError } from './common.js';
|
||
import { handleUIApiRequests, serveStaticFiles } from './ui-manager.js';
|
||
import { handleAPIRequests } from './api-manager.js';
|
||
import { getApiService, getProviderStatus } from './service-manager.js';
|
||
import { getProviderPoolManager } from './service-manager.js';
|
||
import { MODEL_PROVIDER } from './common.js';
|
||
import { PROMPT_LOG_FILENAME } from './config-manager.js';
|
||
import { handleOllamaRequest, handleOllamaShow } from './ollama-handler.js';
|
||
import { getPluginManager } from './plugin-manager.js';
|
||
|
||
/**
|
||
* Parse request body as JSON
|
||
*/
|
||
function parseRequestBody(req) {
|
||
return new Promise((resolve, reject) => {
|
||
let body = '';
|
||
req.on('data', chunk => { body += chunk.toString(); });
|
||
req.on('end', () => {
|
||
try {
|
||
resolve(body ? JSON.parse(body) : {});
|
||
} catch (e) {
|
||
reject(new Error('Invalid JSON in request body'));
|
||
}
|
||
});
|
||
req.on('error', reject);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Main request handler. It authenticates the request, determines the endpoint type,
|
||
* and delegates to the appropriate specialized handler function.
|
||
* @param {Object} config - The server configuration
|
||
* @param {Object} providerPoolManager - The provider pool manager instance
|
||
* @returns {Function} - The request handler function
|
||
*/
|
||
export function createRequestHandler(config, providerPoolManager) {
|
||
return async function requestHandler(req, res) {
|
||
// Deep copy the config for each request to allow dynamic modification
|
||
const currentConfig = deepmerge({}, config);
|
||
const requestUrl = new URL(req.url, `http://${req.headers.host}`);
|
||
let path = requestUrl.pathname;
|
||
const method = req.method;
|
||
|
||
// Set CORS headers for all requests
|
||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-goog-api-key, Model-Provider');
|
||
|
||
// Handle CORS preflight requests
|
||
if (method === 'OPTIONS') {
|
||
res.writeHead(204);
|
||
res.end();
|
||
return;
|
||
}
|
||
|
||
// Serve static files for UI (除了登录页面需要认证)
|
||
// 检查是否是插件静态文件
|
||
const pluginManager = getPluginManager();
|
||
const isPluginStatic = pluginManager.isPluginStaticPath(path);
|
||
if (path.startsWith('/static/') || path === '/' || path === '/favicon.ico' || path === '/index.html' || path.startsWith('/app/') || path === '/login.html' || isPluginStatic) {
|
||
const served = await serveStaticFiles(path, res);
|
||
if (served) return;
|
||
}
|
||
|
||
const uiHandled = await handleUIApiRequests(method, path, req, res, currentConfig, providerPoolManager);
|
||
if (uiHandled) return;
|
||
|
||
// 执行插件路由
|
||
const pluginRouteHandled = await pluginManager.executeRoutes(method, path, req, res);
|
||
if (pluginRouteHandled) return;
|
||
|
||
// Ollama show endpoint with model name
|
||
if (method === 'POST' && path === '/ollama/api/show') {
|
||
await handleOllamaShow(req, res);
|
||
return true;
|
||
}
|
||
|
||
console.log(`\n${new Date().toLocaleString()}`);
|
||
console.log(`[Server] Received request: ${req.method} http://${req.headers.host}${req.url}`);
|
||
|
||
// Health check endpoint
|
||
if (method === 'GET' && path === '/health') {
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
status: 'healthy',
|
||
timestamp: new Date().toISOString(),
|
||
provider: currentConfig.MODEL_PROVIDER
|
||
}));
|
||
return true;
|
||
}
|
||
|
||
// providers health endpoint
|
||
// url params: provider[string], customName[string], unhealthRatioThreshold[float]
|
||
// 支持provider, customName过滤记录
|
||
// 支持unhealthRatioThreshold控制不健康比例的阈值, 当unhealthyRatio超过阈值返回summaryHealthy: false
|
||
if (method === 'GET' && path === '/provider_health') {
|
||
try {
|
||
const provider = requestUrl.searchParams.get('provider');
|
||
const customName = requestUrl.searchParams.get('customName');
|
||
let unhealthRatioThreshold = requestUrl.searchParams.get('unhealthRatioThreshold');
|
||
unhealthRatioThreshold = unhealthRatioThreshold === null ? 0.0001 : parseFloat(unhealthRatioThreshold);
|
||
let provideStatus = await getProviderStatus(currentConfig, { provider, customName });
|
||
let summaryHealth = true;
|
||
if (!isNaN(unhealthRatioThreshold)) {
|
||
summaryHealth = provideStatus.unhealthyRatio <= unhealthRatioThreshold;
|
||
}
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
timestamp: new Date().toISOString(),
|
||
items: provideStatus.providerPoolsSlim,
|
||
count: provideStatus.count,
|
||
unhealthyCount: provideStatus.unhealthyCount,
|
||
unhealthyRatio: provideStatus.unhealthyRatio,
|
||
unhealthySummeryMessage: provideStatus.unhealthySummeryMessage,
|
||
summaryHealth
|
||
}));
|
||
return true;
|
||
} catch (error) {
|
||
console.log(`[Server] req provider_health error: ${error.message}`);
|
||
handleError(res, { statusCode: 500, message: `Failed to get providers health: ${error.message}` }, currentConfig.MODEL_PROVIDER);
|
||
return;
|
||
}
|
||
}
|
||
|
||
|
||
// Handle API requests
|
||
// Allow overriding MODEL_PROVIDER via request header
|
||
const modelProviderHeader = req.headers['model-provider'];
|
||
if (modelProviderHeader) {
|
||
currentConfig.MODEL_PROVIDER = modelProviderHeader;
|
||
console.log(`[Config] MODEL_PROVIDER overridden by header to: ${currentConfig.MODEL_PROVIDER}`);
|
||
}
|
||
|
||
// Check if the first path segment matches a MODEL_PROVIDER and switch if it does
|
||
// Note: 'ollama' is not a valid MODEL_PROVIDER, it's a protocol prefix for Ollama API compatibility
|
||
const pathSegments = path.split('/').filter(segment => segment.length > 0);
|
||
const isOllamaPath = pathSegments[0] === 'ollama' || path.startsWith('/api/');
|
||
|
||
if (pathSegments.length > 0 && !isOllamaPath) {
|
||
const firstSegment = pathSegments[0];
|
||
const isValidProvider = Object.values(MODEL_PROVIDER).includes(firstSegment);
|
||
if (firstSegment && isValidProvider) {
|
||
currentConfig.MODEL_PROVIDER = firstSegment;
|
||
console.log(`[Config] MODEL_PROVIDER overridden by path segment to: ${currentConfig.MODEL_PROVIDER}`);
|
||
pathSegments.shift();
|
||
path = '/' + pathSegments.join('/');
|
||
requestUrl.pathname = path;
|
||
} else if (firstSegment && !isValidProvider) {
|
||
console.log(`[Config] Ignoring invalid MODEL_PROVIDER in path segment: ${firstSegment}`);
|
||
}
|
||
}
|
||
|
||
// 1. 执行认证流程(只有 type='auth' 的插件参与)
|
||
const authResult = await pluginManager.executeAuth(req, res, requestUrl, currentConfig);
|
||
if (authResult.handled) {
|
||
// 认证插件已处理请求(如发送了错误响应)
|
||
return;
|
||
}
|
||
if (!authResult.authorized) {
|
||
// 没有认证插件授权,返回 401
|
||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({ error: { message: 'Unauthorized: API key is invalid or missing.' } }));
|
||
return;
|
||
}
|
||
|
||
// 2. 执行普通中间件(type!='auth' 的插件)
|
||
const middlewareResult = await pluginManager.executeMiddleware(req, res, requestUrl, currentConfig);
|
||
if (middlewareResult.handled) {
|
||
// 中间件已处理请求
|
||
return;
|
||
}
|
||
|
||
// Handle Ollama request BEFORE getting apiService (Ollama endpoints handle their own provider selection)
|
||
// This is important because Ollama /api/tags aggregates models from ALL providers, not just the default one
|
||
if (isOllamaPath) {
|
||
const { handled, normalizedPath } = await handleOllamaRequest(method, path, requestUrl, req, res, null, currentConfig, providerPoolManager);
|
||
if (handled) return;
|
||
// If not handled by Ollama handler, continue with normal flow
|
||
path = normalizedPath;
|
||
}
|
||
|
||
// 获取或选择 API Service 实例
|
||
let apiService;
|
||
try {
|
||
apiService = await getApiService(currentConfig);
|
||
} catch (error) {
|
||
handleError(res, { statusCode: 500, message: `Failed to get API service: ${error.message}` }, currentConfig.MODEL_PROVIDER);
|
||
const poolManager = getProviderPoolManager();
|
||
if (poolManager) {
|
||
poolManager.markProviderUnhealthy(currentConfig.MODEL_PROVIDER, {
|
||
uuid: currentConfig.uuid
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Handle count_tokens requests (Anthropic API compatible)
|
||
if (path.includes('/count_tokens') && method === 'POST') {
|
||
try {
|
||
const body = await parseRequestBody(req);
|
||
console.log(`[Server] Handling count_tokens request for model: ${body.model}`);
|
||
|
||
// Check if apiService has countTokens method
|
||
if (apiService && typeof apiService.countTokens === 'function') {
|
||
const result = apiService.countTokens(body);
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify(result));
|
||
} else {
|
||
// Fallback: use estimateInputTokens if available
|
||
if (apiService && typeof apiService.estimateInputTokens === 'function') {
|
||
const inputTokens = apiService.estimateInputTokens(body);
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({ input_tokens: inputTokens }));
|
||
} else {
|
||
// Last resort: return 0 with a message
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({ input_tokens: 0 }));
|
||
}
|
||
}
|
||
return true;
|
||
} catch (error) {
|
||
console.error(`[Server] count_tokens error: ${error.message}`);
|
||
handleError(res, { statusCode: 500, message: `Failed to count tokens: ${error.message}` }, currentConfig.MODEL_PROVIDER);
|
||
return;
|
||
}
|
||
}
|
||
|
||
try {
|
||
// Handle API requests (Ollama requests are already handled above before apiService is obtained)
|
||
const apiHandled = await handleAPIRequests(method, path, req, res, currentConfig, apiService, providerPoolManager, PROMPT_LOG_FILENAME);
|
||
if (apiHandled) return;
|
||
|
||
// Fallback for unmatched routes
|
||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({ error: { message: 'Not Found' } }));
|
||
} catch (error) {
|
||
handleError(res, error, currentConfig.MODEL_PROVIDER);
|
||
}
|
||
};
|
||
}
|