ikun2 / server.js
bingn's picture
Upload 19 files
f1357b6 verified
/**
* server.js - API ไปฃ็†ๆœๅŠกไธปๅ…ฅๅฃ
*
* ๅฏนๅค–ๆšด้œฒ OpenAI / Anthropic ๅ…ผๅฎน็š„ API ็ซฏ็‚น
* ๅฏนๅ†…้€š่ฟ‡่ดฆๅทๆฑ ่ฝฎๆขไฝฟ็”จ chataibot.pro ่ฏ•็”จ้ขๅบฆ
*
* ๅฏๅŠจ: node server.js
* ็ซฏๅฃ: config.server.port (้ป˜่ฎค 9090)
*/
import http from 'http';
import config from './config.js';
import { AccountPool } from './pool.js';
import { handleChatCompletions } from './openai-adapter.js';
import { handleMessages } from './anthropic-adapter.js';
import { getModelList } from './message-convert.js';
import { getDashboardHTML } from './ui.js';
// ==================== ๅˆๅง‹ๅŒ– ====================
const pool = new AccountPool();
await pool.init();
// ==================== HTTP ๆœๅŠกๅ™จ ====================
function parseBody(req, maxSize = 2 * 1024 * 1024) { // ้ป˜่ฎค 2MB
return new Promise((resolve, reject) => {
let data = '';
let size = 0;
req.on('data', chunk => {
size += chunk.length;
if (size > maxSize) {
req.destroy();
reject(new Error('Request body too large'));
return;
}
data += chunk;
});
req.on('end', () => {
try { resolve(JSON.parse(data)); }
catch { reject(new Error('Invalid JSON body')); }
});
req.on('error', reject);
});
}
function sendJson(res, status, data) {
res.writeHead(status, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
});
res.end(JSON.stringify(data));
}
function sendError(res, status, message) {
sendJson(res, status, {
error: { message, type: 'invalid_request_error', code: status },
});
}
function sendHtml(res, html) {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' });
res.end(html);
}
function checkAuth(req) {
if (!config.server.apiKey) return true;
// Bearer token
const auth = req.headers['authorization'] || '';
if (auth.startsWith('Bearer ') && auth.slice(7) === config.server.apiKey) return true;
// x-api-key header (Anthropic ้ฃŽๆ ผ)
if (req.headers['x-api-key'] === config.server.apiKey) return true;
return false;
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const pathname = url.pathname;
const method = req.method;
// CORS ้ข„ๆฃ€
if (method === 'OPTIONS') {
res.writeHead(204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key, anthropic-version',
'Access-Control-Max-Age': '86400',
});
res.end();
return;
}
// ๅฅๅบทๆฃ€ๆŸฅ (ไธ้œ€่ฆ่ฎค่ฏ)
if (method === 'GET' && pathname === '/health') {
sendJson(res, 200, { status: 'ok', uptime: process.uptime() });
return;
}
// Dashboard ๆŽงๅˆถๅฐ
if (method === 'GET' && (pathname === '/' || pathname === '/ui')) {
sendHtml(res, getDashboardHTML());
return;
}
// ๆจกๅž‹ๅˆ—่กจ (Dashboard ้œ€่ฆๅ…่ฎค่ฏ่ฎฟ้—ฎ)
if (method === 'GET' && pathname === '/v1/models') {
sendJson(res, 200, { object: 'list', data: getModelList() });
return;
}
// ่ดฆๅทๆฑ ็Šถๆ€ (Dashboard ้œ€่ฆๅ…่ฎค่ฏ่ฎฟ้—ฎ)
if (method === 'GET' && pathname === '/pool/status') {
sendJson(res, 200, pool.getStatus());
return;
}
// ๆณจๅ†Œๆ—ฅๅฟ— (Dashboard ้œ€่ฆๅ…่ฎค่ฏ่ฎฟ้—ฎ)
if (method === 'GET' && pathname === '/pool/logs') {
const since = parseInt(url.searchParams.get('since')) || 0;
sendJson(res, 200, { logs: pool.getLogs(since) });
return;
}
// ๆ‰‹ๅŠจๆณจๅ†Œ่ดฆๅท (Dashboard ่ฐƒ็”จ)
if (method === 'POST' && pathname === '/pool/register') {
try {
const body = await parseBody(req);
const count = Math.min(Math.max(1, body.count || 5), 50);
const provider = body.provider || undefined;
const concurrency = body.concurrency ? Math.min(Math.max(1, body.concurrency), 20) : undefined;
const result = pool.manualRegister(count, provider, concurrency);
sendJson(res, 200, { ok: true, message: `ๅทฒๆไบค ${result.queued} ไธชๆณจๅ†ŒไปปๅŠก`, ...result });
} catch (e) {
sendJson(res, 200, { ok: false, message: e.message });
}
return;
}
// ่ฎค่ฏๆ ก้ชŒ
if (!checkAuth(req)) {
sendError(res, 401, 'Invalid API key');
return;
}
try {
// ==================== ่ทฏ็”ฑ ====================
// OpenAI: POST /v1/chat/completions
if (method === 'POST' && pathname === '/v1/chat/completions') {
const body = await parseBody(req);
await handleChatCompletions(body, res, pool);
return;
}
// Anthropic: POST /v1/messages
if (method === 'POST' && pathname === '/v1/messages') {
const body = await parseBody(req);
await handleMessages(body, res, pool);
return;
}
// 404
sendError(res, 404, `Not found: ${method} ${pathname}`);
} catch (e) {
console.error(`[Server] ่ฏทๆฑ‚้”™่ฏฏ: ${e.message}`);
if (!res.headersSent) {
sendError(res, 500, 'Internal server error');
}
}
});
// ==================== ๅฏๅŠจ ====================
const { port, host } = config.server;
server.listen(port, host, () => {
console.log(`\n โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”`);
console.log(` โ”‚ ChatAIBot API Proxy โ”‚`);
console.log(` โ”‚ http://${host}:${port} โ”‚`);
console.log(` โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค`);
console.log(` โ”‚ Dashboard: http://${host}:${port}/ โ”‚`);
console.log(` โ”‚ OpenAI: POST /v1/chat/completions โ”‚`);
console.log(` โ”‚ Anthropic: POST /v1/messages โ”‚`);
console.log(` โ”‚ Models: GET /v1/models โ”‚`);
console.log(` โ”‚ Pool: GET /pool/status โ”‚`);
console.log(` โ”‚ Health: GET /health โ”‚`);
console.log(` โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค`);
console.log(` โ”‚ Auth: ${config.server.apiKey ? 'Bearer ' + config.server.apiKey.substring(0, 8) + '...' : 'ๆ—  (ๅผ€ๆ”พ่ฎฟ้—ฎ)'}${' '.repeat(Math.max(0, 24 - (config.server.apiKey ? 16 : 13)))}โ”‚`);
console.log(` โ”‚ Pool: ${pool.getStatus().active} active / ${pool.getStatus().total} total${' '.repeat(Math.max(0, 22 - String(pool.getStatus().active).length - String(pool.getStatus().total).length))}โ”‚`);
console.log(` โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜\n`);
});
// ไผ˜้›…้€€ๅ‡บ
process.on('SIGINT', () => {
console.log('\n[Server] ๆญฃๅœจๅ…ณ้—ญ...');
pool.destroy();
server.close(() => process.exit(0));
});
process.on('SIGTERM', () => {
pool.destroy();
server.close(() => process.exit(0));
});