| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| 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();
|
|
|
|
|
|
|
| function parseBody(req, maxSize = 2 * 1024 * 1024) {
|
| 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;
|
|
|
| const auth = req.headers['authorization'] || '';
|
| if (auth.startsWith('Bearer ') && auth.slice(7) === config.server.apiKey) return true;
|
|
|
| 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;
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
| if (method === 'GET' && (pathname === '/' || pathname === '/ui')) {
|
| sendHtml(res, getDashboardHTML());
|
| return;
|
| }
|
|
|
|
|
| if (method === 'GET' && pathname === '/v1/models') {
|
| sendJson(res, 200, { object: 'list', data: getModelList() });
|
| return;
|
| }
|
|
|
|
|
| if (method === 'GET' && pathname === '/pool/status') {
|
| sendJson(res, 200, pool.getStatus());
|
| return;
|
| }
|
|
|
|
|
| if (method === 'GET' && pathname === '/pool/logs') {
|
| const since = parseInt(url.searchParams.get('since')) || 0;
|
| sendJson(res, 200, { logs: pool.getLogs(since) });
|
| return;
|
| }
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
| if (method === 'POST' && pathname === '/v1/chat/completions') {
|
| const body = await parseBody(req);
|
| await handleChatCompletions(body, res, pool);
|
| return;
|
| }
|
|
|
|
|
| if (method === 'POST' && pathname === '/v1/messages') {
|
| const body = await parseBody(req);
|
| await handleMessages(body, res, pool);
|
| return;
|
| }
|
|
|
|
|
| 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));
|
| });
|
|
|