diff --git a/.gitignore b/.gitignore index 77b3015..271e28e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ venv/ *.swo src/Authorization.txt +tmp_response*.json diff --git a/scripts/interactive_chat.js b/scripts/interactive_chat.js index 89a00f0..e3e88f8 100644 --- a/scripts/interactive_chat.js +++ b/scripts/interactive_chat.js @@ -4,8 +4,11 @@ import axios from 'axios'; import readline from 'readline'; +import { getMappedModel } from '../src/api/modelMapping.js'; +import { DEFAULT_MODEL } from '../src/config.js'; const API_URL = 'http://localhost:3264/api/chat/completions'; +const CHAT_MODEL = getMappedModel('qwen-max-latest', DEFAULT_MODEL); const rl = readline.createInterface({ input: process.stdin, @@ -31,7 +34,7 @@ async function streamChat(userMessage) { content: userMessage } ], - model: 'qwen-max-latest', + model: CHAT_MODEL, stream: true }, { diff --git a/scripts/run_tests.js b/scripts/run_tests.js index e578e38..7c88455 100644 --- a/scripts/run_tests.js +++ b/scripts/run_tests.js @@ -1,44 +1,67 @@ import axios from 'axios'; import fs from 'fs'; -const base = 'http://localhost:3264/api/chat/completions'; +import { getMappedModel } from '../src/api/modelMapping.js'; + +import { DEFAULT_MODEL } from '../src/config.js'; + +const BASE_URL = 'http://localhost:3264/api/chat/completions'; +const TEST_MODEL = getMappedModel('qwen-max-latest', DEFAULT_MODEL); const headers = { 'User-Agent': 'OpenWebUI-Test/1.0', 'Content-Type': 'application/json' }; +async function checkServer() { + try { + const r = await axios.get('http://localhost:3264/api/health', { timeout: 5000 }); + return r.status === 200; + } catch { + return false; + } +} + +function formatError(e) { + if (e instanceof AggregateError) { + const messages = e.errors.map(err => err.message || err.code || String(err)).join('\n'); + return `AggregateError (${e.errors.length} ошибок):\n${messages}`; + } + if (e.code === 'ECONNREFUSED' || e.code === 'ERR_NETWORK') { + return 'Не удалось подключиться к серверу на localhost:3264. Запустите start.bat сначала.'; + } + let msg = e.message || String(e); + if (e.response?.data) { + try { + const data = typeof e.response.data === 'string' ? e.response.data : JSON.stringify(e.response.data); + msg += `\nОтвет сервера: ${data.slice(0, 500)}`; + } catch {} + } + return msg; +} + async function run() { - try { - console.log('POST 1: Меня зовут Дима'); - const r1 = await axios.post(base, { model: 'qwen-max-latest', messages: [{ role: 'user', content: 'Меня зовут Дима' }] }, { headers, timeout: 120000 }); - console.log('Статус ответа 1:', r1.status); - console.log(JSON.stringify(r1.data, null, 2)); - fs.writeFileSync('./tmp_response1.json', JSON.stringify(r1.data, null, 2), 'utf8'); + if (!await checkServer()) { + console.error('Сервер не запущен на localhost:3264. Запустите start.bat сначала.'); + process.exit(1); + } + + try { + console.log(`POST 1: Меня зовут Дима (модель: ${TEST_MODEL})`); + const r1 = await axios.post(BASE_URL, { model: TEST_MODEL, messages: [{ role: 'user', content: 'Меня зовут Дима' }] }, { headers, timeout: 120000 }); + console.log('Статус:', r1.status); + console.log(JSON.stringify(r1.data, null, 2)); + fs.writeFileSync('./tmp_response1.json', JSON.stringify(r1.data, null, 2), 'utf8'); - await new Promise(r => setTimeout(r, 500)); + await new Promise(r => setTimeout(r, 500)); - console.log('\nPOST 2: Как меня зовут?'); - const r2 = await axios.post(base, { model: 'qwen-max-latest', messages: [{ role: 'user', content: 'Как меня зовут?' }] }, { headers, timeout: 120000 }); - console.log('Статус ответа 2:', r2.status); - console.log(JSON.stringify(r2.data, null, 2)); - fs.writeFileSync('./tmp_response2.json', JSON.stringify(r2.data, null, 2), 'utf8'); + console.log('\nPOST 2: Как меня зовут?'); + const r2 = await axios.post(BASE_URL, { model: TEST_MODEL, messages: [{ role: 'user', content: 'Как меня зовут?' }] }, { headers, timeout: 120000 }); + console.log('Статус:', r2.status); + console.log(JSON.stringify(r2.data, null, 2)); + fs.writeFileSync('./tmp_response2.json', JSON.stringify(r2.data, null, 2), 'utf8'); - console.log('\nSaved responses to tmp_response1.json and tmp_response2.json'); + console.log('\nСохранено в tmp_response1.json и tmp_response2.json'); - // tail logs that may include rawChunks entries - try { - const logs = fs.readFileSync('./logs/server.log', 'utf8'); - const matches = logs.split(/\r?\n/).filter(l => l.includes('[raw]') || l.includes('rawChunks') || l.includes('Ответ получен успешно')); - console.log('\n--- recent raw log lines (filtered) ---'); - console.log(matches.slice(-40).join('\n')); } catch (e) { - console.warn('Could not read server log file (server.log). Listing logs folder instead:'); - console.log(fs.readdirSync('./logs').join('\n')); - } - - } catch (e) { - console.error('Ошибка при запуске тестов:', e.toString()); - if (e.response && e.response.data) { - console.error('Данные ответа:', JSON.stringify(e.response.data, null, 2)); + console.error('Ошибка при запуске тестов:', formatError(e)); } - } } -run(); +run(); \ No newline at end of file diff --git a/scripts/test_streaming.js b/scripts/test_streaming.js index 668039e..e88ac1f 100644 --- a/scripts/test_streaming.js +++ b/scripts/test_streaming.js @@ -1,143 +1,139 @@ -/** - * Test streaming API - */ - import axios from 'axios'; +import { getMappedModel } from '../src/api/modelMapping.js'; +import { DEFAULT_MODEL } from '../src/config.js'; const API_URL = 'http://localhost:3264/api/chat/completions'; +const TEST_MODEL = getMappedModel('qwen-max-latest', DEFAULT_MODEL); + +function formatError(e) { + if (e instanceof AggregateError) { + const messages = e.errors.map(err => err.message || err.code || String(err)).join('\n'); + return `AggregateError (${e.errors.length}):\n${messages}`; + } + return e.message || String(e); +} + +async function checkServer() { + try { + const r = await axios.get('http://localhost:3264/api/health', { timeout: 5000 }); + return r.status === 200; + } catch { + return false; + } +} async function testStreaming() { + if (!await checkServer()) { + console.error('Сервер не запущен. Запустите start.bat.'); + process.exit(1); + } + try { console.log('=== Testing Streaming API ===\n'); - - // First request: introduce yourself - console.log('POST 1: Stream "Привет, я Дима"'); - + console.log(`POST 1: Stream "Привет, я Дима" (модель: ${TEST_MODEL})`); + const response1 = await axios.post( API_URL, { - messages: [ - { - role: 'user', - content: 'Привет, я Дима' - } - ], - model: 'qwen-max-latest', + messages: [{ role: 'user', content: 'Привет, я Дима' }], + model: TEST_MODEL, stream: true }, { - headers: { - 'User-Agent': 'TestClient/1.0' - }, + headers: { 'User-Agent': 'TestClient/1.0', 'Content-Type': 'application/json' }, responseType: 'stream' } ); - // Handle streaming response let fullContent1 = ''; let chunkCount1 = 0; - + await new Promise((resolve, reject) => { response1.data.on('data', (chunk) => { const text = chunk.toString(); const lines = text.split('\n').filter(line => line.trim()); - + for (const line of lines) { if (line.startsWith('data: ')) { try { const json = JSON.parse(line.substring(6)); if (json.choices?.[0]?.delta?.content) { const content = json.choices[0].delta.content; - process.stdout.write(content); // Real-time output + process.stdout.write(content); fullContent1 += content; chunkCount1++; } - } catch (e) { - // Ignore parse errors - } + } catch {} } } }); - + response1.data.on('end', () => { console.log('\n'); - console.log(`✅ Stream 1 complete. Chunks: ${chunkCount1}, Total length: ${fullContent1.length}`); + console.log(`Stream 1 complete. Chunks: ${chunkCount1}, Length: ${fullContent1.length}`); resolve(); }); - + response1.data.on('error', reject); }); - // Wait a bit before second request await new Promise(resolve => setTimeout(resolve, 2000)); - // Second request: ask about name console.log('\nPOST 2: Stream "Как меня зовут?"'); - const response2 = await axios.post( API_URL, { - messages: [ - { - role: 'user', - content: 'Как меня зовут?' - } - ], - model: 'qwen-max-latest', + messages: [{ role: 'user', content: 'Как меня зовут?' }], + model: TEST_MODEL, stream: true }, { - headers: { - 'User-Agent': 'TestClient/1.0' - }, + headers: { 'User-Agent': 'TestClient/1.0', 'Content-Type': 'application/json' }, responseType: 'stream' } ); - // Handle streaming response let fullContent2 = ''; let chunkCount2 = 0; - + await new Promise((resolve, reject) => { response2.data.on('data', (chunk) => { const text = chunk.toString(); const lines = text.split('\n').filter(line => line.trim()); - + for (const line of lines) { if (line.startsWith('data: ')) { try { const json = JSON.parse(line.substring(6)); if (json.choices?.[0]?.delta?.content) { const content = json.choices[0].delta.content; - process.stdout.write(content); // Real-time output + process.stdout.write(content); fullContent2 += content; chunkCount2++; } - } catch (e) { - // Ignore parse errors - } + } catch {} } } }); - + response2.data.on('end', () => { console.log('\n'); - console.log(`✅ Stream 2 complete. Chunks: ${chunkCount2}, Total length: ${fullContent2.length}`); + console.log(`Stream 2 complete. Chunks: ${chunkCount2}, Length: ${fullContent2.length}`); resolve(); }); - + response2.data.on('error', reject); }); console.log('\n=== Streaming Test Successful ==='); } catch (error) { - console.error('Ошибка:', error.message); + console.error('Ошибка:', formatError(error)); if (error.response) { console.error('Status:', error.response.status); - console.error('Data:', error.response.data); + try { console.error('Data:', typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data)); } catch {} } } } -testStreaming(); +testStreaming(); \ No newline at end of file diff --git a/src/AvailableModels.txt b/src/AvailableModels.txt index 6b8bb19..8ee3c7e 100644 --- a/src/AvailableModels.txt +++ b/src/AvailableModels.txt @@ -12,7 +12,6 @@ qwen3-vl-plus qwen3-coder-plus qwen3-omni-flash qwen3-omni-flash-2025-12-01 -qwen-max-latest qwen-plus-2025-09-11 qwen-plus-2025-01-25 qwq-32b @@ -25,4 +24,4 @@ qvq-72b-preview-0310 qwen2.5-vl-32b-instruct qwen2.5-14b-instruct-1m qwen2.5-coder-32b-instruct -qwen2.5-72b-instruct +qwen2.5-72b-instruct \ No newline at end of file diff --git a/src/api/chat.js b/src/api/chat.js index 4d19743..4cff0e2 100644 --- a/src/api/chat.js +++ b/src/api/chat.js @@ -706,7 +706,11 @@ async function executeApiRequest(page, apiUrl, payload, token, onChunk = null) { async function handleApiError(response, tokenObj, message, model, chatId, parentId, files, retryCount, chatType, size, waitForCompletion, onChunk = null) { logRaw(JSON.stringify(response)); - logError(`Ошибка при получении ответа: ${response.error || response.statusText}`); + const errorMessage = response.error || response.statusText || (response.status ? `HTTP ${response.status}` : 'Неизвестная ошибка'); + logError(`Ошибка при получении ответа: ${errorMessage}`); + if (!response.error && !response.statusText && response.errorBody) { + logDebug(`Ответ с ошибкой без statusText: ${response.errorBody}`); + } if (response.errorBody) logDebug(`Тело ответа с ошибкой: ${response.errorBody}`); if (response.html && response.html.includes('Verification')) { @@ -729,7 +733,8 @@ async function handleApiError(response, tokenObj, message, model, chatId, parent } const { hasValidTokens } = await import('./tokenManager.js'); if (hasValidTokens() && retryCount < MAX_RETRY_COUNT) { - return sendMessage(message, model, chatId, parentId, files, null, null, null, chatType, size, waitForCompletion, retryCount + 1, onChunk); + logInfo('Пересоздаем чат с новым токеном'); + return sendMessage(message, model, null, null, files, null, null, null, chatType, size, waitForCompletion, retryCount + 1, onChunk); } logError('Не осталось валидных токенов или исчерпаны попытки.'); return { error: 'Все токены недействительны (401). Требуется повторная авторизация.', chatId }; @@ -753,7 +758,8 @@ async function handleApiError(response, tokenObj, message, model, chatId, parent authToken = null; const { hasValidTokens } = await import('./tokenManager.js'); if (hasValidTokens() && retryCount < MAX_RETRY_COUNT) { - return sendMessage(message, model, chatId, parentId, files, null, null, null, chatType, size, waitForCompletion, retryCount + 1, onChunk); + logInfo('Пересоздаем чат с новым токеном'); + return sendMessage(message, model, null, null, files, null, null, null, chatType, size, waitForCompletion, retryCount + 1, onChunk); } return { error: `Все токены заблокированы по лимиту (${hours}ч)`, chatId }; } @@ -766,8 +772,14 @@ async function handleApiError(response, tokenObj, message, model, chatId, parent export async function sendMessage(message, model = DEFAULT_MODEL, chatId = null, parentId = null, files = null, tools = null, toolChoice = null, systemMessage = null, chatType = 't2t', size = null, waitForCompletion = true, retryCount = 0, onChunk = null) { if (!availableModels) availableModels = getAvailableModelsFromFile(); + const browserContext = getBrowserContext(); + if (!browserContext) return { error: 'Браузер не инициализирован', chatId }; + + const tokenObj = await resolveAuthToken(browserContext); + if (!tokenObj) return { error: 'Ошибка авторизации: не удалось получить токен', chatId }; + if (!chatId) { - const newChatResult = await createChatV2(model, 'Новый чат', 0, chatType); + const newChatResult = await createChatV2(model, 'Новый чат', 0, chatType, tokenObj); if (newChatResult.error) return { error: 'Не удалось создать чат: ' + newChatResult.error }; chatId = newChatResult.chatId; logInfo(`Создан новый чат v2 с ID: ${chatId}`); @@ -792,12 +804,6 @@ export async function sendMessage(message, model = DEFAULT_MODEL, chatId = null, logInfo(`Тип генерации: ${chatType} (${typeLabels[chatType] || chatType})${size ? `, размер: ${size}` : ''}`); } - const browserContext = getBrowserContext(); - if (!browserContext) return { error: 'Браузер не инициализирован', chatId }; - - const tokenObj = await resolveAuthToken(browserContext); - if (!tokenObj) return { error: 'Ошибка авторизации: не удалось получить токен', chatId }; - let page = null; try { page = await pagePool.getPage(browserContext); @@ -1005,11 +1011,11 @@ export function getAuthToken() { // ─── createChatV2 ──────────────────────────────────────────────────────────── -export async function createChatV2(model = DEFAULT_MODEL, title = 'Новый чат', retryCount = 0, chatType = 't2t') { +export async function createChatV2(model = DEFAULT_MODEL, title = 'Новый чат', retryCount = 0, chatType = 't2t', preselectedToken = null) { const browserContext = getBrowserContext(); if (!browserContext) return { error: 'Браузер не инициализирован' }; - const tokenObj = await getAvailableToken(); + const tokenObj = preselectedToken || await getAvailableToken(); if (tokenObj?.token) { authToken = tokenObj.token; logInfo(`Используется аккаунт для создания чата: ${tokenObj.id}`); diff --git a/src/api/modelMapping.js b/src/api/modelMapping.js index b4ee944..053335b 100644 --- a/src/api/modelMapping.js +++ b/src/api/modelMapping.js @@ -11,7 +11,6 @@ const CANONICAL_MODELS = Object.freeze([ "qwen3-coder-plus", "qwen3-omni-flash", "qwen3-omni-flash-2025-12-01", - "qwen-max-latest", "qwen-plus-2025-09-11", "qwen-plus-2025-01-25", "qwq-32b", @@ -73,7 +72,10 @@ const ALIAS_GROUPS = Object.freeze({ "Qwen3-Max", "Qwen3-Maximum", "qwen3-max-preview", - "Qwen3-Max-Preview" + "Qwen3-Max-Preview", + "qwen-max-latest", + "Qwen2.5-Max", + "qwen2.5-max" ], "qwen3-vl-plus": [ "qwen-vl", @@ -133,14 +135,6 @@ const ALIAS_GROUPS = Object.freeze({ "qwen3-coder-30b", "Qwen3-Coder-30B-A3B-Instruct" ], - "qwen-max-latest": [ - "Qwen2.5-Max", - "qwen2.5-max" - ], - "qwen-plus-2025-01-25": [ - "Qwen2.5-Plus", - "qwen2.5-plus" - ], "qwq-32b": [ "qwq", "QwQ-32B", @@ -229,26 +223,17 @@ const buildModelMapping = () => { export const MODEL_MAPPING = buildModelMapping(); -/** - * Получить соответствующую доступную модель - * @param {string} requestedModel - Запрошенная модель - * @param {string} defaultModel - Модель по умолчанию - * @returns {string} - Доступная модель - */ export function getMappedModel(requestedModel, defaultModel = DEFAULT_MODEL) { if (!requestedModel) return defaultModel; - // Проверяем точное соответствие в словаре if (MODEL_MAPPING[requestedModel]) { return MODEL_MAPPING[requestedModel]; } - // Проверяем, является ли запрошенная модель уже доступной const availableModels = Object.values(MODEL_MAPPING); if (availableModels.includes(requestedModel)) { return requestedModel; } - // Возвращаем модель по умолчанию, если соответствие не найдено return defaultModel; -} \ No newline at end of file +} \ No newline at end of file diff --git a/test_api.bat b/test_api.bat index 0e9472c..407a82c 100644 --- a/test_api.bat +++ b/test_api.bat @@ -1,16 +1,17 @@ @echo off -REM Simple batch script to test the API +chcp 65001 >nul +title FreeQwenApi - API Test echo. -echo ╔════════════════════════════════════════╗ +echo ╔══════════════════════════════════════╗ echo ║ FreeQwenApi - API Test ║ -echo ╚════════════════════════════════════════╝ +echo ╚══════════════════════════════════════╝ echo. echo Запуск Node.js тестов... echo. -cd /d D:\Users\Dima\Desktop\FreeQwenApi +cd /d "%~dp0" echo Test 1: Обычный запрос (non-streaming) node scripts/run_tests.js @@ -24,7 +25,7 @@ echo Test 3: Интерактивный чат (введите exit для вы node scripts/interactive_chat.js echo. -echo ╔════════════════════════════════════════╗ +echo ╔══════════════════════════════════════╗ echo ║ All tests complete! ║ -echo ╚════════════════════════════════════════╝ -pause +echo ╚══════════════════════════════════════╝ +pause \ No newline at end of file