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