import express from 'express'; import bodyParser from 'body-parser'; import { fileURLToPath } from 'url'; import path from 'path'; import readline from 'readline'; import { initBrowser, shutdownBrowser } from './src/browser/browser.js'; import apiRoutes from './src/api/routes.js'; import { getAvailableModelsFromFile, getApiKeys } from './src/api/chat.js'; import { loadTokens } from './src/api/tokenManager.js'; import { addAccountInteractive } from './src/utils/accountSetup.js'; import { logHttpRequest, logInfo, logError, logWarn } from './src/logger/index.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const DEFAULT_PORT = 3264; const port = Number.parseInt(process.env.PORT ?? DEFAULT_PORT, 10); const host = process.env.HOST || '0.0.0.0'; if (Number.isNaN(port) || port <= 0 || port > 65535) { throw new Error(`Некорректное значение переменной PORT: ${process.env.PORT}`); } const skipAccountMenu = toBoolean(process.env.SKIP_ACCOUNT_MENU) || toBoolean(process.env.NON_INTERACTIVE); function prompt(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise(res => rl.question(question, ans => { rl.close(); res(ans.trim()); })); } function toBoolean(value) { if (typeof value !== 'string') return false; return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase()); } function ensureNonInteractiveTokens() { const tokens = loadTokens(); if (!tokens.length) { logError('Не найдено ни одного аккаунта. Запустите скрипт авторизации перед запуском сервера.'); process.exit(1); } const now = Date.now(); const validTokens = tokens.filter(t => (!t.resetAt || new Date(t.resetAt).getTime() <= now) && !t.invalid); if (!validTokens.length) { logError('Все аккаунты недоступны. Перезапустите авторизацию перед запуском сервера.'); process.exit(1); } logInfo(`Автоматический запуск: обнаружено ${tokens.length} аккаунтов, из них ${validTokens.length} активны.`); } // Middleware для логирования HTTP-запросов app.use(logHttpRequest); app.use(bodyParser.json({ limit: '150mb' })); app.use(bodyParser.urlencoded({ limit: '150mb', extended: true })); app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); }); app.use('/api', apiRoutes); // Обработчик 404 app.use((req, res) => { logWarn(`404 Not Found: ${req.method} ${req.originalUrl}`); res.status(404).json({ error: 'Эндпоинт не найден' }); }); // Обработчик ошибок app.use((err, req, res, next) => { logError('Внутренняя ошибка сервера', err); res.status(500).json({ error: 'Внутренняя ошибка сервера' }); }); process.on('SIGINT', handleShutdown); process.on('SIGTERM', handleShutdown); process.on('SIGHUP', handleShutdown); process.on('uncaughtException', async (error) => { logError('Необработанное исключение', error); await handleShutdown(); }); async function handleShutdown() { logInfo('\nПолучен сигнал завершения. Закрываем браузер...'); await shutdownBrowser(); logInfo('Завершение работы.'); process.exit(0); } async function startServer() { console.log(` ███████ ██████ ███████ ███████ ██████ ██ ██ ███████ ███ ██ █████ ██████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ █████ ██████ █████ █████ ██ ██ ██ █ ██ █████ ██ ██ ██ ███████ ██████ ██ ██ ██ ██ ██ ██ ██ ▄▄ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ ███████ ██████ ███ ███ ███████ ██ ████ ██ ██ ██ ██ ▀▀ API-прокси для Qwen `); logInfo('Запуск сервера...'); if (!skipAccountMenu) { // Меню управления аккаунтами перед запуском прокси while (true) { const tokens = loadTokens(); console.log('\nСписок аккаунтов:'); if (!tokens.length) { console.log(' (пусто)'); } else { tokens.forEach((token, i) => { const now = Date.now(); const isInvalid = token.invalid === true; const isWaiting = Boolean(token.resetAt && new Date(token.resetAt).getTime() > now); const statusCode = isInvalid ? 0 : isWaiting ? 1 : 2; const statusLabel = isInvalid ? '❌ Недействителен' : isWaiting ? '⏳ Ожидание сброса' : '✅ OK'; console.log(`${String(i + 1).padStart(2, ' ')} | ${token.id} | ${statusLabel} (${statusCode})`); }); } console.log('\n=== Меню ==='); console.log('1 - Добавить новый аккаунт'); console.log('2 - Перелогинить аккаунт с истекшим токеном'); console.log('3 - Запустить прокси (по умолчанию)'); console.log('4 - Удалить аккаунт'); let choice = await prompt('Ваш выбор (Enter = 3): '); if (!choice) choice = '3'; if (choice === '1') { await addAccountInteractive(); } else if (choice === '2') { const { reloginAccountInteractive } = await import('./src/utils/accountSetup.js'); await reloginAccountInteractive(); } else if (choice === '3') { const hasValidToken = tokens.some(token => { if (token.invalid) return false; if (!token.resetAt) return true; return new Date(token.resetAt).getTime() <= Date.now(); }); if (!tokens.length || !hasValidToken) { console.log('Нужен хотя бы один валидный аккаунт для запуска.'); continue; } break; } else if (choice === '4') { const { removeAccountInteractive } = await import('./src/utils/accountSetup.js'); await removeAccountInteractive(); } } } else { ensureNonInteractiveTokens(); } //===================================================================================================== //const sim = await prompt('Смоделировать ошибку RateLimited для первого запроса? (y/N): '); //if (sim.toLowerCase() === 'y') { // global.simulateRateLimit = true; // } //===================================================================================================== const browserInitialized = await initBrowser(false); if (!browserInitialized) { logError('Не удалось инициализировать браузер. Завершение работы.'); process.exit(1); } try { app.listen(port, host, () => { const displayHost = host === '0.0.0.0' ? 'localhost' : host; logInfo(`Сервер запущен на ${host}:${port}`); logInfo(`API доступен по адресу: http://${displayHost}:${port}/api`); logInfo('Для проверки статуса авторизации: GET /api/status'); logInfo('Для отправки сообщения: POST /api/chat'); logInfo('Для получения списка моделей: GET /api/models'); logInfo('======================================================'); logInfo('API v2 - История чатов хранится на серверах Qwen'); logInfo('Создать новый чат: POST /api/chats'); logInfo('Отправить сообщение: POST /api/chat (с chatId и parentId)'); logInfo('======================================================'); logInfo('Доступно 19 моделей Qwen (через систему маппинга):'); logInfo('- Стандартные: qwen-max, qwen-plus, qwen-turbo и их latest-версии'); logInfo('- Coder: qwen3-coder-plus, qwen2.5-coder-*b-instruct (0.5b - 32b)'); logInfo('- Визуальные: qwen-vl-max, qwen-vl-plus и их latest-версии'); logInfo('- Qwen 3: qwen3, qwen3-max, qwen3-plus, qwen3-omni-flash'); logInfo('======================================================'); logInfo('Формат JSON запроса на чат:'); logInfo('{ "message": "текст сообщения", "model": "название модели (опционально)", "chatId": "ID чата (опционально)", "parentId": "ID родительского сообщения (опционально)" }'); logInfo('Пример первого запроса: { "message": "Привет, как дела?" }'); logInfo('Пример второго запроса: { "message": "А что ты умеешь?", "chatId": "полученный_id_чата", "parentId": "полученный_parentId" }'); logInfo('======================================================'); logInfo('Поддержка OpenAI совместимого API: POST /api/chat/completions'); logInfo('В ответе возвращаются chatId и parentId для продолжения диалога'); logInfo('======================================================'); getApiKeys(); getAvailableModelsFromFile(); }); } catch (err) { if (err.code === 'EADDRINUSE') { logError(`Порт ${port} уже используется. Возможно, сервер уже запущен.`); logError('Завершите работу существующего сервера или используйте другой порт.'); await shutdownBrowser(); process.exit(1); } else { throw err; } } } startServer().catch(async error => { logError('Ошибка при запуске сервера:', error); await shutdownBrowser(); process.exit(1); });