QwenAI / src /api /routes.js
imseldrith's picture
Initial upload from Google Colab
9de864e verified
// 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;