import { getBrowserContext, getAuthenticationStatus, setAuthenticationStatus } from '../browser/browser.js'; import { checkAuthentication } from '../browser/auth.js'; import { checkVerification } from '../browser/auth.js'; import { shutdownBrowser, initBrowser } from '../browser/browser.js'; import { saveAuthToken } from '../browser/session.js'; import { getAvailableToken, markRateLimited, removeInvalidToken } from './tokenManager.js'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { logRaw } from '../logger/index.js'; import crypto from 'crypto'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const CHAT_API_URL_V2 = 'https://chat.qwen.ai/api/v2/chat/completions'; const CREATE_CHAT_URL = 'https://chat.qwen.ai/api/v2/chats/new'; const CHAT_PAGE_URL = 'https://chat.qwen.ai/'; const MODELS_FILE = path.join(__dirname, '..', 'AvaibleModels.txt'); const AUTH_KEYS_FILE = path.join(__dirname, '..', 'Authorization.txt'); let authToken = null; let availableModels = null; let authKeys = null; const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); async function getPage(context) { if (context && typeof context.goto === 'function') { return context; } else if (context && typeof context.newPage === 'function') { const page = await context.newPage(); return page; } else { throw new Error('Неверный контекст: не страница Puppeteer, не контекст Playwright'); } } export const pagePool = { pages: [], maxSize: 3, async getPage(context) { if (this.pages.length > 0) { return this.pages.pop(); } const newPage = await getPage(context); await newPage.goto(CHAT_PAGE_URL, { waitUntil: 'domcontentloaded', timeout: 120000 }); if (!authToken) { try { authToken = await newPage.evaluate(() => localStorage.getItem('token')); console.log('Токен авторизации получен из браузера'); if (authToken) { saveAuthToken(authToken); } } catch (e) { console.error('Ошибка при получении токена авторизации:', e); } } return newPage; }, releasePage(page) { if (this.pages.length < this.maxSize) { this.pages.push(page); } else { page.close().catch(e => console.error('Ошибка при закрытии страницы:', e)); } }, async clear() { for (const page of this.pages) { try { await page.close(); } catch (e) { console.error('Ошибка при закрытии страницы в пуле:', e); } } this.pages = []; } }; export async function extractAuthToken(context, forceRefresh = false) { if (authToken && !forceRefresh) { return authToken; } try { const page = await getPage(context); try { await page.goto(CHAT_PAGE_URL, { waitUntil: 'domcontentloaded', timeout: 120000 }); await delay(2000); const newToken = await page.evaluate(() => localStorage.getItem('token')); if (typeof context.newPage === 'function') { await page.close(); } if (newToken) { authToken = newToken; console.log('Токен авторизации успешно извлечен'); saveAuthToken(authToken); return authToken; } else { console.error('Токен авторизации не найден в браузере'); return null; } } catch (error) { if (typeof context.newPage === 'function') { await page.close().catch(() => {}); } throw error; } } catch (error) { console.error('Ошибка при извлечении токена авторизации:', error); return null; } } export function getAvailableModelsFromFile() { try { if (!fs.existsSync(MODELS_FILE)) { console.error(`Файл с моделями не найден: ${MODELS_FILE}`); return ['qwen-max-latest']; } const fileContent = fs.readFileSync(MODELS_FILE, 'utf8'); const models = fileContent.split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); console.log('===== ДОСТУПНЫЕ МОДЕЛИ ====='); models.forEach(model => console.log(`- ${model}`)); console.log('============================'); return models; } catch (error) { console.error('Ошибка при чтении файла с моделями:', error); return ['qwen-max-latest']; } } function getAuthKeysFromFile() { try { if (!fs.existsSync(AUTH_KEYS_FILE)) { const template = `# Файл API-ключей для прокси\n# --------------------------------------------\n# В этом файле перечислены токены, которые\n# прокси будет считать «действительными».\n# Один ключ — одна строка без пробелов.\n#\n# 1) Хотите ОТКЛЮЧИТЬ авторизацию целиком?\n# Оставьте файл пустым — сервер перестанет\n# проверять заголовок Authorization.\n#\n# 2) Хотите разрешить доступ нескольким людям?\n# Впишите каждый ключ в отдельной строке:\n# d35ab3e1-a6f9-4d...\n# f2b1cd9c-1b2e-4a...\n#\n# Пустые строки и строки, начинающиеся с «#»,\n# игнорируются.`; try { fs.writeFileSync(AUTH_KEYS_FILE, template, { encoding: 'utf8', flag: 'wx' }); console.log(`Создан шаблон файла ключей: ${AUTH_KEYS_FILE}`); } catch (e) { console.error('Не удалось создать шаблон Authorization.txt:', e); } return []; } const fileContent = fs.readFileSync(AUTH_KEYS_FILE, 'utf8'); const keys = fileContent.split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); return keys; } catch (error) { console.error('Ошибка при чтении файла с ключами авторизации:', error); return []; } } export function isValidModel(modelName) { if (!availableModels) { availableModels = getAvailableModelsFromFile(); } return availableModels.includes(modelName); } export function getAllModels() { if (!availableModels) { availableModels = getAvailableModelsFromFile(); } return { models: availableModels.map(model => ({ id: model, name: model, description: `Модель ${model}` })) }; } export function getApiKeys() { if (!authKeys) { authKeys = getAuthKeysFromFile(); } return authKeys; } export async function sendMessage(message, model = "qwen-max-latest", chatId = null, parentId = null, files = null, tools = null, toolChoice = null, systemMessage = null) { if (!availableModels) { availableModels = getAvailableModelsFromFile(); } // Создаём новый чат, если не передан if (!chatId) { const newChatResult = await createChatV2(model); if (newChatResult.error) { return { error: 'Не удалось создать чат: ' + newChatResult.error }; } chatId = newChatResult.chatId; console.log(`Создан новый чат v2 с ID: ${chatId}`); } // Валидация сообщения let messageContent = message; try { if (message === null || message === undefined) { console.error('Сообщение пустое'); return { error: 'Сообщение не может быть пустым', chatId }; } else if (typeof message === 'string') { messageContent = message; } else if (Array.isArray(message)) { const isValid = message.every(item => (item.type === 'text' && typeof item.text === 'string') || (item.type === 'image' && typeof item.image === 'string') || (item.type === 'file' && typeof item.file === 'string') ); if (!isValid) { console.error('Некорректная структура составного сообщения'); return { error: 'Некорректная структура составного сообщения', chatId }; } messageContent = message; } else { console.error('Неподдерживаемый формат сообщения:', message); return { error: 'Неподдерживаемый формат сообщения', chatId }; } } catch (error) { console.error('Ошибка при обработке сообщения:', error); return { error: 'Ошибка при обработке сообщения: ' + error.message, chatId }; } if (!model || model.trim() === "") { model = "qwen-max-latest"; } else { if (!isValidModel(model)) { console.warn(`Предупреждение: Указанная модель "${model}" не найдена в списке доступных моделей. Используется модель по умолчанию.`); model = "qwen-max-latest"; } } console.log(`Используемая модель: "${model}"`); let tokenObj = await getAvailableToken(); if (tokenObj && tokenObj.token) { authToken = tokenObj.token; console.log(`Используется аккаунт: ${tokenObj.id}`); } const browserContext = getBrowserContext(); if (!browserContext) { return { error: 'Браузер не инициализирован', chatId }; } if (!getAuthenticationStatus()) { console.log('Проверка авторизации...'); const authCheck = await checkAuthentication(browserContext); if (!authCheck) { return { error: 'Требуется авторизация. Пожалуйста, авторизуйтесь в открытом браузере.', chatId }; } } if (!authToken) { console.log('Получение токена авторизации...'); authToken = await extractAuthToken(browserContext); if (!authToken) { console.error('Не удалось получить токен авторизации'); return { error: 'Ошибка авторизации: не удалось получить токен', chatId }; } } let page = null; try { page = await pagePool.getPage(browserContext); const verificationNeeded = await checkVerification(page); if (verificationNeeded) { await page.reload({ waitUntil: 'domcontentloaded', timeout: 120000 }); } if (!authToken) { console.error('Токен отсутствует перед отправкой запроса'); authToken = await page.evaluate(() => localStorage.getItem('token')); if (!authToken) { return { error: 'Токен авторизации не найден. Требуется перезапуск в ручном режиме.', chatId }; } else { saveAuthToken(authToken); } } console.log('Отправка запроса к API v2...'); // Формируем новое сообщение для v2 API const userMessageId = crypto.randomUUID(); const assistantChildId = crypto.randomUUID(); const newMessage = { fid: userMessageId, parentId: parentId, parent_id: parentId, role: "user", content: messageContent, chat_type: "t2t", sub_chat_type: "t2t", timestamp: Math.floor(Date.now() / 1000), user_action: "chat", models: [model], files: files || [], childrenIds: [assistantChildId], extra: { meta: { subChatType: "t2t" } }, feature_config: { thinking_enabled: false, output_schema: "phase" } }; // Формируем payload для v2 API const payload = { stream: true, incremental_output: true, chat_id: chatId, chat_mode: "normal", messages: [newMessage], model: model, parent_id: parentId, timestamp: Math.floor(Date.now() / 1000) }; // Добавляем system message если есть if (systemMessage) { payload.system_message = systemMessage; console.log(`System message: ${systemMessage.substring(0, 100)}${systemMessage.length > 100 ? '...' : ''}`); } // Добавляем tools если есть if (tools && Array.isArray(tools) && tools.length > 0) { payload.tools = tools; payload.tool_choice = toolChoice || "auto"; } console.log('=== PAYLOAD V2 ===\n' + JSON.stringify(payload, null, 2)); console.log(`Отправка сообщения в чат ${chatId} с parent_id: ${parentId || 'null'}`); const apiUrl = `${CHAT_API_URL_V2}?chat_id=${chatId}`; const evalData = { apiUrl: apiUrl, payload: payload, token: authToken }; console.log(`Используем токен: ${authToken ? 'Токен существует' : 'Токен отсутствует'}`); console.log(`API URL: ${apiUrl}`); // Выполняем запрос через браузер и парсим SSE let response = await page.evaluate(async (data) => { try { const token = data.token; if (!token) { return { success: false, error: 'Токен авторизации не найден' }; } const response = await fetch(data.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, 'Accept': '*/*' }, body: JSON.stringify(data.payload) }); if (response.ok) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let fullContent = ''; let responseId = null; let usage = null; let finished = false; while (!finished) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.trim() || !line.startsWith('data: ')) continue; const jsonStr = line.substring(6).trim(); if (!jsonStr) continue; try { const chunk = JSON.parse(jsonStr); // Первый чанк с метаданными if (chunk['response.created']) { responseId = chunk['response.created'].response_id; } // Чанки с контентом if (chunk.choices && chunk.choices[0]) { const delta = chunk.choices[0].delta; if (delta && delta.content) { fullContent += delta.content; } if (delta && delta.status === 'finished') { finished = true; } } // Обновляем usage if (chunk.usage) { usage = chunk.usage; } } catch (e) { // Игнорируем ошибки парсинга отдельных чанков } } } return { success: true, data: { id: responseId || 'chatcmpl-' + Date.now(), object: 'chat.completion', created: Math.floor(Date.now() / 1000), model: data.payload.model, choices: [{ index: 0, message: { role: 'assistant', content: fullContent }, finish_reason: 'stop' }], usage: usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }, response_id: responseId } }; } else { const errorBody = await response.text(); return { success: false, status: response.status, statusText: response.statusText, errorBody: errorBody }; } } catch (error) { return { success: false, error: error.toString() }; } }, evalData); // --- TEST: симуляция ответа RateLimited --- if (global.simulateRateLimit && !global.__rateLimitedTested) { global.__rateLimitedTested = true; response = { success: false, status: 429, errorBody: JSON.stringify({ code: 'RateLimited', detail: "You've reached the upper limit for today's usage.", template: 'You have reached the daily usage limit. Please wait {{num}} hours before trying again.', num: 4 }) }; console.log('*** Симуляция ответа RateLimited активирована ***'); } pagePool.releasePage(page); page = null; if (response.success) { // Логируем сырой ответ от модели logRaw(JSON.stringify(response.data)); console.log('Ответ получен успешно'); // Добавляем метаданные для клиента response.data.chatId = chatId; response.data.parentId = response.data.response_id; // Для следующего сообщения response.data.id = response.data.id || "chatcmpl-" + Date.now(); return response.data; } else { // Логируем ошибочный сырой ответ logRaw(JSON.stringify(response)); console.error('Ошибка при получении ответа:', response.error || response.statusText); if (response.errorBody) { console.error('Тело ответа с ошибкой:', response.errorBody); } if (response.html && response.html.includes('Verification')) { setAuthenticationStatus(false); console.log('Обнаружена необходимость верификации, перезапуск браузера в видимом режиме...'); await pagePool.clear(); authToken = null; await shutdownBrowser(); await initBrowser(true); return { error: 'Требуется верификация. Браузер запущен в видимом режиме.', verification: true, chatId }; } // ----- Новая обработка истекшего токена / 401 Unauthorized ----- if ((response.status === 401) || (response.errorBody && (response.errorBody.includes('Unauthorized') || response.errorBody.includes('Token has expired')))) { console.log('Токен', tokenObj?.id, 'недействителен (401). Удаляем и пробуем другой.'); // Удаляем токен из пула authToken = null; if (tokenObj && tokenObj.id) { const { markInvalid } = await import('./tokenManager.js'); markInvalid(tokenObj.id); } // Есть ли ещё токены? const { hasValidTokens } = await import('./tokenManager.js'); if (hasValidTokens()) { return await sendMessage(message, model, chatId, files); // повторяем с новым токеном } console.error('Не осталось валидных токенов. Останавливаю прокси.'); await pagePool.clear(); await shutdownBrowser(); process.exit(1); } if (response.errorBody && response.errorBody.includes('RateLimited')) { try { const rateInfo = JSON.parse(response.errorBody); const hours = Number(rateInfo.num) || 24; if (tokenObj && tokenObj.id) { markRateLimited(tokenObj.id, hours); console.log(`Токен ${tokenObj.id} достиг лимита. Помечаем на ${hours}ч и пробуем другой токен...`); } } catch (e) { console.error('Не удалось распарсить тело ошибки RateLimited:', e); } authToken = null; return await sendMessage(message, model, chatId, files); } return { error: response.error || response.statusText, details: response.errorBody || 'Нет дополнительных деталей', chatId }; } } catch (error) { console.error('Ошибка при отправке сообщения:', error); return { error: error.toString(), chatId }; } finally { if (page) { try { if (typeof getBrowserContext().newPage === 'function') { await page.close(); } } catch (e) { console.error('Ошибка при закрытии страницы:', e); } } } } export async function clearPagePool() { await pagePool.clear(); } export function getAuthToken() { return authToken; } export async function listModels(browserContext) { return await getAvailableModels(browserContext); } // Создание нового чата через v2 API export async function createChatV2(model = "qwen-max-latest", title = "Новый чат") { const browserContext = getBrowserContext(); if (!browserContext) { return { error: 'Браузер не инициализирован' }; } // Получаем токен из tokenManager let tokenObj = await getAvailableToken(); if (tokenObj && tokenObj.token) { authToken = tokenObj.token; console.log(`Используется аккаунт для создания чата: ${tokenObj.id}`); } if (!authToken) { console.log('Получение токена авторизации для создания чата...'); authToken = await extractAuthToken(browserContext); if (!authToken) { return { error: 'Не удалось получить токен авторизации' }; } } let page = null; try { page = await pagePool.getPage(browserContext); const payload = { title: title, models: [model], chat_mode: "normal", chat_type: "t2t", timestamp: Date.now() }; const evalData = { apiUrl: CREATE_CHAT_URL, payload: payload, token: authToken }; const result = await page.evaluate(async (data) => { try { const response = await fetch(data.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${data.token}` }, body: JSON.stringify(data.payload) }); if (response.ok) { const result = await response.json(); return { success: true, data: result }; } else { const errorBody = await response.text(); return { success: false, status: response.status, errorBody: errorBody }; } } catch (error) { return { success: false, error: error.toString() }; } }, evalData); pagePool.releasePage(page); page = null; if (result.success && result.data.success) { console.log(`Чат создан: ${result.data.data.id}`); return { success: true, chatId: result.data.data.id, requestId: result.data.request_id }; } else { console.error('Ошибка при создании чата:', result); return { error: result.errorBody || result.error || 'Неизвестная ошибка' }; } } catch (error) { console.error('Ошибка при создании чата:', error); return { error: error.toString() }; } finally { if (page) { try { if (typeof getBrowserContext().newPage === 'function') { await page.close(); } } catch (e) { console.error('Ошибка при закрытии страницы:', e); } } } } export async function testToken(token) { const browserContext = getBrowserContext(); if (!browserContext) return 'ERROR'; let page; try { page = await getPage(browserContext); await page.goto(CHAT_PAGE_URL, { waitUntil: 'domcontentloaded' }); const evalData = { apiUrl: CHAT_API_URL_V2, token, payload: { chat_type: 't2t', messages: [{ role: 'user', content: 'ping', chat_type: 't2t' }], model: 'qwen-max-latest', stream: false } }; const result = await page.evaluate(async (data) => { try { const res = await fetch(data.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${data.token}` }, body: JSON.stringify(data.payload) }); return { ok: res.ok, status: res.status }; } catch (e) { return { ok: false, status: 0, error: e.toString() }; } }, evalData); if (result.ok || result.status === 400) return 'OK'; if (result.status === 401 || result.status === 403) return 'UNAUTHORIZED'; if (result.status === 429) return 'RATELIMIT'; return 'ERROR'; } catch (e) { console.error('testToken error:', e); return 'ERROR'; } finally { if (page) { try { if (typeof browserContext.newPage === 'function') { await page.close(); } } catch { } } } }