Spaces:
Paused
Paused
| // routes.js - Модуль с маршрутами для API | |
| import express from 'express'; | |
| import { sendMessage, getAllModels, getApiKeys, createChatV2 } from './chat.js'; | |
| import { getAuthenticationStatus } from '../browser/browser.js'; | |
| import { checkAuthentication } from '../browser/auth.js'; | |
| import { getBrowserContext } from '../browser/browser.js'; | |
| import { logInfo, logError, logDebug } from '../logger/index.js'; | |
| import { getMappedModel } from './modelMapping.js'; | |
| import { getStsToken, uploadFileToQwen } from './fileUpload.js'; | |
| import multer from 'multer'; | |
| import path from 'path'; | |
| import fs from 'fs'; | |
| import crypto from 'crypto'; | |
| import { listTokens, markInvalid, markRateLimited, markValid } from './tokenManager.js'; | |
| import { testToken } from './chat.js'; | |
| const router = express.Router(); | |
| // Настройка multer для загрузки файлов | |
| const storage = multer.diskStorage({ | |
| destination: function (req, file, cb) { | |
| const uploadDir = path.join(process.cwd(), 'uploads'); | |
| if (!fs.existsSync(uploadDir)) { | |
| fs.mkdirSync(uploadDir, { recursive: true }); | |
| } | |
| cb(null, uploadDir); | |
| }, | |
| filename: function (req, file, cb) { | |
| const uniqueSuffix = Date.now() + '-' + crypto.randomBytes(8).toString('hex'); | |
| cb(null, uniqueSuffix + '-' + file.originalname); | |
| } | |
| }); | |
| const upload = multer({ | |
| storage: storage, | |
| limits: { fileSize: 10 * 1024 * 1024 } // 10MB макс. размер | |
| }); | |
| function authMiddleware(req, res, next) { | |
| const apiKeys = getApiKeys(); | |
| if (apiKeys.length === 0) { | |
| return next(); | |
| } | |
| const authHeader = req.headers.authorization; | |
| const apiKeyHeaderPrefix = 'Bearer '; | |
| if (!authHeader || !authHeader.startsWith(apiKeyHeaderPrefix)) { | |
| logError('Отсутствует или некорректный заголовок авторизации'); | |
| return res.status(401).json({ error: 'Требуется авторизация' }); | |
| } | |
| const token = authHeader.substring(apiKeyHeaderPrefix.length).trim(); | |
| if (!apiKeys.includes(token)) { | |
| logError('Предоставлен недействительный API ключ'); | |
| return res.status(401).json({ error: 'Недействительный токен' }); | |
| } | |
| next(); | |
| } | |
| router.use(authMiddleware); | |
| router.use((req, res, next) => { | |
| req.url = req.url | |
| .replace(/\/v[12](?=\/|$)/g, '') | |
| .replace(/\/+/g, '/'); | |
| next(); | |
| }); | |
| router.post('/chat', async (req, res) => { | |
| try { | |
| const { message, messages, model, chatId, parentId } = req.body; | |
| // Поддержка как message, так и messages для совместимости | |
| let messageContent = message; | |
| let systemMessage = null; | |
| // Если указан параметр messages (множественное число), используем его в приоритете | |
| if (messages && Array.isArray(messages)) { | |
| // Извлекаем system message если есть | |
| const systemMsg = messages.find(msg => msg.role === 'system'); | |
| if (systemMsg) { | |
| systemMessage = systemMsg.content; | |
| } | |
| // Преобразуем формат messages в формат сообщения, понятный нашему прокси | |
| if (messages.length > 0) { | |
| const lastUserMessage = messages.filter(msg => msg.role === 'user').pop(); | |
| if (lastUserMessage) { | |
| if (Array.isArray(lastUserMessage.content)) { | |
| messageContent = lastUserMessage.content; | |
| } else { | |
| messageContent = lastUserMessage.content; | |
| } | |
| } | |
| } | |
| } | |
| if (!messageContent) { | |
| logError('Запрос без сообщения'); | |
| return res.status(400).json({ error: 'Сообщение не указано' }); | |
| } | |
| logInfo(`Получен запрос: ${typeof messageContent === 'string' ? messageContent.substring(0, 50) + (messageContent.length > 50 ? '...' : '') : 'Составное сообщение'}`); | |
| if (systemMessage) { | |
| logInfo(`System message: ${systemMessage.substring(0, 50)}${systemMessage.length > 50 ? '...' : ''}`); | |
| } | |
| if (chatId) { | |
| logInfo(`Используется chatId: ${chatId}, parentId: ${parentId || 'null'}`); | |
| } | |
| let mappedModel = model || "qwen-max-latest"; | |
| if (model) { | |
| mappedModel = getMappedModel(model); | |
| if (mappedModel !== model) { | |
| logInfo(`Модель "${model}" заменена на "${mappedModel}"`); | |
| } | |
| } | |
| logInfo(`Используется модель: ${mappedModel}`); | |
| const result = await sendMessage(messageContent, mappedModel, chatId, parentId, null, null, null, systemMessage); | |
| if (result.choices && result.choices[0] && result.choices[0].message) { | |
| const responseLength = result.choices[0].message.content ? result.choices[0].message.content.length : 0; | |
| logInfo(`Ответ успешно сформирован для запроса, длина ответа: ${responseLength}`); | |
| } else if (result.error) { | |
| logInfo(`Получена ошибка в ответе: ${result.error}`); | |
| } | |
| res.json(result); | |
| } catch (error) { | |
| logError('Ошибка при обработке запроса', error); | |
| res.status(500).json({ error: 'Внутренняя ошибка сервера' }); | |
| } | |
| }); | |
| router.get('/models', async (req, res) => { | |
| try { | |
| logInfo('Запрос на получение списка моделей'); | |
| const modelsRaw = getAllModels(); | |
| const openAiModels = { | |
| object: 'list', | |
| data: modelsRaw.models.map(m => ({ | |
| id: m.id || m.name || m, | |
| object: 'model', | |
| created: 0, | |
| owned_by: 'openai', | |
| permission: [] | |
| })) | |
| }; | |
| logInfo(`Возвращено ${openAiModels.data.length} моделей (OpenAI формат)`); | |
| res.json(openAiModels); | |
| } catch (error) { | |
| logError('Ошибка при получении списка моделей', error); | |
| res.status(500).json({ error: 'Внутренняя ошибка сервера' }); | |
| } | |
| }); | |
| router.get('/status', async (req, res) => { | |
| try { | |
| logInfo('Запрос статуса авторизации'); | |
| const tokens = listTokens(); | |
| const accounts = await Promise.all(tokens.map(async t => { | |
| const accInfo = { id: t.id, status: 'UNKNOWN', resetAt: t.resetAt || null }; | |
| if (t.resetAt) { | |
| const resetTime = new Date(t.resetAt).getTime(); | |
| if (resetTime > Date.now()) { | |
| accInfo.status = 'WAIT'; | |
| return accInfo; | |
| } | |
| } | |
| const testResult = await testToken(t.token); | |
| if (testResult === 'OK') { | |
| accInfo.status = 'OK'; | |
| if (t.invalid || t.resetAt) markValid(t.id); | |
| } else if (testResult === 'RATELIMIT') { | |
| accInfo.status = 'WAIT'; | |
| markRateLimited(t.id, 24); | |
| } else if (testResult === 'UNAUTHORIZED') { | |
| accInfo.status = 'INVALID'; | |
| if (!t.invalid) markInvalid(t.id); | |
| } else { | |
| accInfo.status = 'ERROR'; | |
| } | |
| return accInfo; | |
| })); | |
| const browserContext = getBrowserContext(); | |
| if (!browserContext) { | |
| logError('Браузер не инициализирован'); | |
| return res.json({ authenticated: false, message: 'Браузер не инициализирован', accounts }); | |
| } | |
| if (getAuthenticationStatus()) { | |
| return res.json({ | |
| accounts | |
| }); | |
| } | |
| await checkAuthentication(browserContext); | |
| const isAuthenticated = getAuthenticationStatus(); | |
| logInfo(`Статус авторизации: ${isAuthenticated ? 'активна' : 'требуется авторизация'}`); | |
| res.json({ | |
| authenticated: isAuthenticated, | |
| message: isAuthenticated ? 'Авторизация активна' : 'Требуется авторизация', | |
| accounts | |
| }); | |
| } catch (error) { | |
| logError('Ошибка при проверке статуса авторизации', error); | |
| res.status(500).json({ error: 'Внутренняя ошибка сервера' }); | |
| } | |
| }); | |
| router.post('/chats', async (req, res) => { | |
| try { | |
| const { name, model } = req.body; | |
| const chatModel = model ? getMappedModel(model) : 'qwen-max-latest'; | |
| logInfo(`Создание нового чата${name ? ` с именем: ${name}` : ''}, модель: ${chatModel}`); | |
| const result = await createChatV2(chatModel, name || "Новый чат"); | |
| if (result.error) { | |
| logError(`Ошибка создания чата: ${result.error}`); | |
| return res.status(500).json({ error: result.error }); | |
| } | |
| logInfo(`Создан новый чат v2 с ID: ${result.chatId}`); | |
| res.json({ chatId: result.chatId, success: true }); | |
| } catch (error) { | |
| logError('Ошибка при создании чата', error); | |
| res.status(500).json({ error: 'Внутренняя ошибка сервера' }); | |
| } | |
| }); | |
| router.post('/analyze/network', (req, res) => { | |
| try { | |
| return res.json({ success: true }); | |
| } catch (error) { | |
| logError('Ошибка при анализе сети', error); | |
| res.status(500).json({ error: 'Внутренняя ошибка сервера' }); | |
| } | |
| }) | |
| router.post('/chat/completions', async (req, res) => { | |
| try { | |
| const { messages, model, stream, tools, functions, tool_choice, chatId, parentId } = req.body; | |
| logInfo(`Получен OpenAI-совместимый запрос${stream ? ' (stream)' : ''}`); | |
| if (!messages || !Array.isArray(messages) || messages.length === 0) { | |
| logError('Запрос без сообщений'); | |
| return res.status(400).json({ error: 'Сообщения не указаны' }); | |
| } | |
| // Извлекаем system message если есть | |
| const systemMsg = messages.find(msg => msg.role === 'system'); | |
| const systemMessage = systemMsg ? systemMsg.content : null; | |
| const lastUserMessage = messages.filter(msg => msg.role === 'user').pop(); | |
| if (!lastUserMessage) { | |
| logError('В запросе нет сообщений от пользователя'); | |
| return res.status(400).json({ error: 'В запросе нет сообщений от пользователя' }); | |
| } | |
| const messageContent = lastUserMessage.content; | |
| let mappedModel = model ? getMappedModel(model) : "qwen-max-latest"; | |
| if (model && mappedModel !== model) { | |
| logInfo(`Модель "${model}" заменена на "${mappedModel}"`); | |
| } | |
| logInfo(`Используется модель: ${mappedModel}`); | |
| if (systemMessage) { | |
| logInfo(`System message: ${systemMessage.substring(0, 50)}${systemMessage.length > 50 ? '...' : ''}`); | |
| } | |
| if (stream) { | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| const writeSse = (payload) => { | |
| res.write('data: ' + JSON.stringify(payload) + '\n\n'); | |
| }; | |
| writeSse({ | |
| id: 'chatcmpl-stream', | |
| object: 'chat.completion.chunk', | |
| created: Math.floor(Date.now() / 1000), | |
| model: mappedModel || 'qwen-max-latest', | |
| choices: [ | |
| { index: 0, delta: { role: 'assistant' }, finish_reason: null } | |
| ] | |
| }); | |
| try { | |
| const combinedTools = tools || (functions ? functions.map(fn => ({ type: 'function', function: fn })) : null); | |
| const result = await sendMessage(messageContent, mappedModel, chatId, parentId, null, combinedTools, tool_choice, systemMessage); | |
| if (result.error) { | |
| writeSse({ | |
| id: 'chatcmpl-stream', | |
| object: 'chat.completion.chunk', | |
| created: Math.floor(Date.now() / 1000), | |
| model: mappedModel || 'qwen-max-latest', | |
| choices: [ | |
| { index: 0, delta: { content: `Error: ${result.error}` }, finish_reason: null } | |
| ] | |
| }); | |
| } else if (result.choices && result.choices[0] && result.choices[0].message) { | |
| const content = String(result.choices[0].message.content || ''); | |
| const codePoints = Array.from(content); | |
| const chunkSize = 16; | |
| for (let i = 0; i < codePoints.length; i += chunkSize) { | |
| const chunk = codePoints.slice(i, i + chunkSize).join(''); | |
| writeSse({ | |
| id: 'chatcmpl-stream', | |
| object: 'chat.completion.chunk', | |
| created: Math.floor(Date.now() / 1000), | |
| model: mappedModel || 'qwen-max-latest', | |
| choices: [ | |
| { index: 0, delta: { content: chunk }, finish_reason: null } | |
| ] | |
| }); | |
| await new Promise(resolve => setTimeout(resolve, 20)); | |
| } | |
| } | |
| writeSse({ | |
| id: 'chatcmpl-stream', | |
| object: 'chat.completion.chunk', | |
| created: Math.floor(Date.now() / 1000), | |
| model: mappedModel || 'qwen-max-latest', | |
| choices: [ | |
| { index: 0, delta: {}, finish_reason: 'stop' } | |
| ] | |
| }); | |
| res.write('data: [DONE]\n\n'); | |
| res.end(); | |
| } catch (error) { | |
| logError('Ошибка при обработке потокового запроса', error); | |
| writeSse({ | |
| id: 'chatcmpl-stream', | |
| object: 'chat.completion.chunk', | |
| created: Math.floor(Date.now() / 1000), | |
| model: mappedModel || 'qwen-max-latest', | |
| choices: [ | |
| { index: 0, delta: { content: 'Internal server error' }, finish_reason: 'stop' } | |
| ] | |
| }); | |
| res.write('data: [DONE]\n\n'); | |
| res.end(); | |
| } | |
| } else { | |
| const combinedTools = tools || (functions ? functions.map(fn => ({ type: 'function', function: fn })) : null); | |
| const result = await sendMessage(messageContent, mappedModel, chatId, parentId, null, combinedTools, tool_choice, systemMessage); | |
| if (result.error) { | |
| return res.status(500).json({ | |
| error: { message: result.error, type: "server_error" } | |
| }); | |
| } | |
| const openaiResponse = { | |
| id: result.id || "chatcmpl-" + Date.now(), | |
| object: "chat.completion", | |
| created: Math.floor(Date.now() / 1000), | |
| model: result.model || mappedModel || "qwen-max-latest", | |
| choices: result.choices || [{ | |
| index: 0, | |
| message: { | |
| role: "assistant", | |
| content: result.choices?.[0]?.message?.content || "" | |
| }, | |
| finish_reason: "stop" | |
| }], | |
| usage: result.usage || { | |
| prompt_tokens: 0, | |
| completion_tokens: 0, | |
| total_tokens: 0 | |
| }, | |
| chatId: result.chatId, | |
| parentId: result.parentId | |
| }; | |
| res.json(openaiResponse); | |
| } | |
| } catch (error) { | |
| logError('Ошибка при обработке запроса', error); | |
| res.status(500).json({ error: { message: 'Внутренняя ошибка сервера', type: "server_error" } }); | |
| } | |
| }); | |
| // Новый маршрут для получения STS токена | |
| router.post('/files/getstsToken', async (req, res) => { | |
| try { | |
| logInfo(`Запрос на получение STS токена: ${JSON.stringify(req.body)}`); | |
| const fileInfo = req.body; | |
| if (!fileInfo || !fileInfo.filename || !fileInfo.filesize || !fileInfo.filetype) { | |
| logError('Некорректные данные о файле'); | |
| return res.status(400).json({ error: 'Некорректные данные о файле' }); | |
| } | |
| const stsToken = await getStsToken(fileInfo); | |
| res.json(stsToken); | |
| } catch (error) { | |
| logError('Ошибка при получении STS токена', error); | |
| res.status(500).json({ error: 'Внутренняя ошибка сервера' }); | |
| } | |
| }); | |
| // Маршрут для загрузки файла - работает | |
| router.post('/files/upload', upload.single('file'), async (req, res) => { | |
| try { | |
| if (!req.file) { | |
| logError('Файл не был загружен'); | |
| return res.status(400).json({ error: 'Файл не был загружен' }); | |
| } | |
| logInfo(`Файл загружен на сервер: ${req.file.originalname} (${req.file.size} байт)`); | |
| // Загружаем файл в Qwen OSS хранилище | |
| const result = await uploadFileToQwen(req.file.path); | |
| // Удаляем временный файл после успешной загрузки | |
| fs.unlinkSync(req.file.path); | |
| if (result.success) { | |
| logInfo(`Файл успешно загружен в OSS: ${result.fileName}`); | |
| res.json({ | |
| success: true, | |
| file: { | |
| name: result.fileName, | |
| url: result.url, | |
| size: req.file.size, | |
| type: req.file.mimetype | |
| } | |
| }); | |
| } else { | |
| logError(`Ошибка при загрузке файла в OSS: ${result.error}`); | |
| res.status(500).json({ error: 'Ошибка при загрузке файла' }); | |
| } | |
| } catch (error) { | |
| logError('Ошибка при загрузке файла', error); | |
| // Удаляем временный файл в случае ошибки | |
| if (req.file && req.file.path && fs.existsSync(req.file.path)) { | |
| fs.unlinkSync(req.file.path); | |
| } | |
| res.status(500).json({ error: 'Внутренняя ошибка сервера' }); | |
| } | |
| }); | |
| export default router; |