qwen-api-proxy 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +2054 -0
- package/bin/qwen-api-proxy.js +414 -0
- package/index.js +444 -0
- package/package.json +85 -0
- package/src/Authorization.txt +17 -0
- package/src/AvailableModels.txt +26 -0
- package/src/api/chat.js +1392 -0
- package/src/api/chatHistory.js +344 -0
- package/src/api/fileUpload.js +182 -0
- package/src/api/imageGeneration.js +459 -0
- package/src/api/modelMapping.js +274 -0
- package/src/api/routes.js +2160 -0
- package/src/api/tokenManager.js +382 -0
- package/src/browser/auth.js +171 -0
- package/src/browser/browser.js +233 -0
- package/src/browser/session.js +134 -0
- package/src/config.js +116 -0
- package/src/logger/index.js +89 -0
- package/src/utils/accountSetup.js +153 -0
- package/src/utils/botSettings.js +231 -0
- package/src/utils/permissionChecker.js +205 -0
- package/src/utils/prompt.js +11 -0
- package/src/utils/proxy.js +255 -0
- package/src/utils/telegramBot.js +2977 -0
- package/src/utils/telegramNotifier.js +94 -0
package/src/api/chat.js
ADDED
|
@@ -0,0 +1,1392 @@
|
|
|
1
|
+
import { getBrowserContext, getAuthenticationStatus, setAuthenticationStatus } from '../browser/browser.js';
|
|
2
|
+
import { checkAuthentication, checkVerification } from '../browser/auth.js';
|
|
3
|
+
import { shutdownBrowser, initBrowser } from '../browser/browser.js';
|
|
4
|
+
import { saveAuthToken } from '../browser/session.js';
|
|
5
|
+
import { getAvailableToken, markRateLimited, removeInvalidToken, checkTokenExpiry, checkAllTokensExpiry, getSafeToken } from './tokenManager.js';
|
|
6
|
+
import { sendTelegramNotification, formatTokenExpiryMessage } from '../utils/telegramNotifier.js';
|
|
7
|
+
import { getActiveModel } from '../utils/botSettings.js';
|
|
8
|
+
import { fetchWithQwenProxy } from '../utils/proxy.js';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { logInfo, logError, logWarn, logDebug, logRaw } from '../logger/index.js';
|
|
13
|
+
import crypto from 'crypto';
|
|
14
|
+
import {
|
|
15
|
+
CHAT_API_URL, CREATE_CHAT_URL, CHAT_PAGE_URL, TASK_STATUS_URL,
|
|
16
|
+
PAGE_TIMEOUT, RETRY_DELAY, PAGE_POOL_SIZE,
|
|
17
|
+
DEFAULT_MODEL, MAX_RETRY_COUNT,
|
|
18
|
+
TASK_POLL_MAX_ATTEMPTS, TASK_POLL_INTERVAL, TOKEN_EXPIRY_WARNING_MS,
|
|
19
|
+
MODELS_API_URL
|
|
20
|
+
} from '../config.js';
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = path.dirname(__filename);
|
|
24
|
+
|
|
25
|
+
const MODELS_FILE = path.join(__dirname, '..', 'AvailableModels.txt');
|
|
26
|
+
const AUTH_KEYS_FILE = path.join(__dirname, '..', 'Authorization.txt');
|
|
27
|
+
|
|
28
|
+
let authToken = null;
|
|
29
|
+
let availableModels = null;
|
|
30
|
+
let modelsLoadedFromAPI = false; // Флаг: загружены ли модели из API
|
|
31
|
+
let modelsFetchPromise = null; // Промис для предотвращения параллельных запросов
|
|
32
|
+
let authKeys = null;
|
|
33
|
+
let browserTokenRateLimited = false;
|
|
34
|
+
let resolvedDefaultModel = null; // Будет установлен при загрузке моделей
|
|
35
|
+
|
|
36
|
+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
37
|
+
|
|
38
|
+
// ─── Page helpers ────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
async function getPage(context) {
|
|
41
|
+
if (context && typeof context.newPage === 'function') {
|
|
42
|
+
return await context.newPage();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (context && typeof context.goto === 'function') {
|
|
46
|
+
// Если передана Puppeteer Page, не переиспользуем её как рабочую:
|
|
47
|
+
// создаём отдельную вкладку из того же браузера, чтобы избежать гонок
|
|
48
|
+
// и случайного закрытия базовой страницы.
|
|
49
|
+
if (typeof context.browser === 'function') {
|
|
50
|
+
try {
|
|
51
|
+
const browser = context.browser();
|
|
52
|
+
if (browser && typeof browser.newPage === 'function') {
|
|
53
|
+
return await browser.newPage();
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logWarn(`Не удалось создать новую страницу из текущего контекста: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof context.isClosed === 'function' && context.isClosed()) {
|
|
61
|
+
throw new Error('Базовая страница браузера закрыта');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return context;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new Error('Неверный контекст: не страница Puppeteer, не контекст Playwright');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const pagePool = {
|
|
71
|
+
pages: [],
|
|
72
|
+
maxSize: PAGE_POOL_SIZE,
|
|
73
|
+
|
|
74
|
+
async getPage(context) {
|
|
75
|
+
const baseContext = getBrowserContext();
|
|
76
|
+
while (this.pages.length > 0) {
|
|
77
|
+
const page = this.pages.pop();
|
|
78
|
+
try {
|
|
79
|
+
if (page === baseContext) {
|
|
80
|
+
logWarn('Базовая страница не должна быть в пуле, пропускаем');
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (page.isClosed()) {
|
|
84
|
+
logWarn('Страница из пула закрыта, пропускаем');
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
await page.evaluate(() => document.readyState);
|
|
88
|
+
return page;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
logWarn(`Страница из пула протухла (${e.message?.substring(0, 60)}), создаём новую`);
|
|
91
|
+
if (page !== baseContext) {
|
|
92
|
+
try { await page.close(); } catch { /* already dead */ }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const newPage = await getPage(context);
|
|
98
|
+
await newPage.goto(CHAT_PAGE_URL, { waitUntil: 'domcontentloaded', timeout: PAGE_TIMEOUT });
|
|
99
|
+
|
|
100
|
+
if (!authToken) {
|
|
101
|
+
try {
|
|
102
|
+
authToken = await newPage.evaluate(() => localStorage.getItem('token'));
|
|
103
|
+
logInfo('Токен авторизации получен из браузера');
|
|
104
|
+
if (authToken) {
|
|
105
|
+
saveAuthToken(authToken);
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
logError('Ошибка при получении токена авторизации', e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return newPage;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
releasePage(page) {
|
|
116
|
+
try {
|
|
117
|
+
if (page.isClosed()) return;
|
|
118
|
+
} catch { return; }
|
|
119
|
+
|
|
120
|
+
const baseContext = getBrowserContext();
|
|
121
|
+
if (page === baseContext) {
|
|
122
|
+
// Базовую страницу держим отдельно от пула.
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (this.pages.length < this.maxSize) {
|
|
127
|
+
this.pages.push(page);
|
|
128
|
+
} else {
|
|
129
|
+
page.close().catch(e => logError('Ошибка при закрытии страницы', e));
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async clear() {
|
|
134
|
+
const baseContext = getBrowserContext();
|
|
135
|
+
for (const page of this.pages) {
|
|
136
|
+
if (page === baseContext) continue;
|
|
137
|
+
try { await page.close(); } catch (e) {
|
|
138
|
+
logError('Ошибка при закрытии страницы в пуле', e);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
this.pages = [];
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// ─── Task polling ────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
export async function pollTaskStatus(taskId, page, token, maxAttempts = TASK_POLL_MAX_ATTEMPTS, interval = TASK_POLL_INTERVAL) {
|
|
148
|
+
logInfo(`Начинаем опрос статуса задачи: ${taskId}`);
|
|
149
|
+
|
|
150
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
151
|
+
try {
|
|
152
|
+
const statusUrl = `${TASK_STATUS_URL}/${taskId}`;
|
|
153
|
+
|
|
154
|
+
const result = await page.evaluate(async (data) => {
|
|
155
|
+
try {
|
|
156
|
+
const response = await fetch(data.url, {
|
|
157
|
+
method: 'GET',
|
|
158
|
+
headers: {
|
|
159
|
+
'Authorization': `Bearer ${data.token}`,
|
|
160
|
+
'Accept': 'application/json'
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
return { success: false, status: response.status, error: await response.text() };
|
|
165
|
+
}
|
|
166
|
+
return { success: true, data: await response.json() };
|
|
167
|
+
} catch (e) {
|
|
168
|
+
return { success: false, error: e.toString() };
|
|
169
|
+
}
|
|
170
|
+
}, { url: statusUrl, token });
|
|
171
|
+
|
|
172
|
+
if (!result.success) {
|
|
173
|
+
logWarn(`Ошибка при проверке статуса (попытка ${attempt}/${maxAttempts}): ${result.error}`);
|
|
174
|
+
if (attempt < maxAttempts) await delay(interval);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const taskData = result.data;
|
|
179
|
+
const taskStatus = taskData.task_status || taskData.status || 'unknown';
|
|
180
|
+
logDebug(`Статус задачи (${attempt}/${maxAttempts}): ${taskStatus}`);
|
|
181
|
+
|
|
182
|
+
if (taskStatus === 'completed' || taskStatus === 'success') {
|
|
183
|
+
logInfo('Задача завершена успешно');
|
|
184
|
+
return { success: true, status: 'completed', data: taskData };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (taskStatus === 'failed' || taskStatus === 'error') {
|
|
188
|
+
logError('Задача завершилась с ошибкой');
|
|
189
|
+
return { success: false, status: 'failed', error: taskData.error || taskData.message || 'Task failed', data: taskData };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (attempt < maxAttempts) await delay(interval);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
logError(`Ошибка при опросе задачи (попытка ${attempt}/${maxAttempts})`, error);
|
|
195
|
+
if (attempt < maxAttempts) await delay(interval);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
logError(`Превышен лимит попыток (${maxAttempts}) для задачи ${taskId}`);
|
|
200
|
+
return { success: false, status: 'timeout', error: 'Task polling timeout exceeded' };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── Token extraction ────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
export async function extractAuthToken(context, forceRefresh = false) {
|
|
206
|
+
if (authToken && !forceRefresh) return authToken;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const page = await getPage(context);
|
|
210
|
+
const shouldClosePage = page !== context;
|
|
211
|
+
try {
|
|
212
|
+
await page.goto(CHAT_PAGE_URL, { waitUntil: 'domcontentloaded', timeout: PAGE_TIMEOUT });
|
|
213
|
+
await delay(RETRY_DELAY);
|
|
214
|
+
|
|
215
|
+
const newToken = await page.evaluate(() => localStorage.getItem('token'));
|
|
216
|
+
if (shouldClosePage) await page.close();
|
|
217
|
+
|
|
218
|
+
if (newToken) {
|
|
219
|
+
authToken = newToken;
|
|
220
|
+
logInfo('Токен авторизации успешно извлечен');
|
|
221
|
+
saveAuthToken(authToken);
|
|
222
|
+
return authToken;
|
|
223
|
+
}
|
|
224
|
+
logError('Токен авторизации не найден в браузере');
|
|
225
|
+
return null;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (shouldClosePage) await page.close().catch(() => { });
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
logError('Ошибка при извлечении токена авторизации', error);
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─── Models & keys from files ────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
export async function fetchModelsFromAPI() {
|
|
239
|
+
try {
|
|
240
|
+
logInfo('🔍 Загрузка списка моделей с Qwen API...');
|
|
241
|
+
logDebug(`MODELS_API_URL: ${MODELS_API_URL}`);
|
|
242
|
+
|
|
243
|
+
// Добавляем параметры для получения большего количества моделей
|
|
244
|
+
// Пробуем разные варианты URL с параметрами пагинации
|
|
245
|
+
let modelsUrl = MODELS_API_URL;
|
|
246
|
+
|
|
247
|
+
// Если URL не содержит параметров, добавляем их
|
|
248
|
+
if (!MODELS_API_URL.includes('?')) {
|
|
249
|
+
// Qwen API использует limit для пагинации
|
|
250
|
+
const params = new URLSearchParams({
|
|
251
|
+
limit: '1000' // Максимальное количество моделей
|
|
252
|
+
});
|
|
253
|
+
modelsUrl = `${MODELS_API_URL}?${params.toString()}`;
|
|
254
|
+
logDebug(`URL с параметрами пагинации: ${modelsUrl}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
logInfo(`Пытаемся получить модели с URL: ${modelsUrl}`);
|
|
258
|
+
|
|
259
|
+
// Пробуем получить через node fetch (без браузера)
|
|
260
|
+
const response = await fetchWithQwenProxy(modelsUrl, {
|
|
261
|
+
method: 'GET',
|
|
262
|
+
headers: {
|
|
263
|
+
'Accept': 'application/json',
|
|
264
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
265
|
+
},
|
|
266
|
+
timeout: 10000
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
logDebug(`API Response status: ${response.status}`);
|
|
270
|
+
logDebug(`API Response ok: ${response.ok}`);
|
|
271
|
+
|
|
272
|
+
if (response.ok) {
|
|
273
|
+
const data = await response.json();
|
|
274
|
+
logDebug(`API Response type: ${Array.isArray(data) ? 'array' : typeof data}`);
|
|
275
|
+
|
|
276
|
+
if (!Array.isArray(data)) {
|
|
277
|
+
logDebug(`API Response keys: ${Object.keys(data).join(', ')}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Логируем структуру для отладки
|
|
281
|
+
logDebug(`data.data существует: ${!!data.data}`);
|
|
282
|
+
if (data.data) {
|
|
283
|
+
logDebug(`data.data type: ${typeof data.data}`);
|
|
284
|
+
logDebug(`data.data is array: ${Array.isArray(data.data)}`);
|
|
285
|
+
if (typeof data.data === 'object' && !Array.isArray(data.data)) {
|
|
286
|
+
logDebug(`data.data keys: ${Object.keys(data.data).join(', ')}`);
|
|
287
|
+
if (data.data.data) {
|
|
288
|
+
logDebug(`data.data.data is array: ${Array.isArray(data.data.data)}`);
|
|
289
|
+
logDebug(`data.data.data length: ${data.data.data.length}`);
|
|
290
|
+
}
|
|
291
|
+
} else if (Array.isArray(data.data)) {
|
|
292
|
+
logDebug(`data.data length: ${data.data.length}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Пробуем разные форматы ответа
|
|
297
|
+
let models = [];
|
|
298
|
+
if (Array.isArray(data)) {
|
|
299
|
+
models = data;
|
|
300
|
+
logDebug(`Извлечены модели: данные - массив (${data.length} элементов)`);
|
|
301
|
+
} else if (data.data && data.data.data && Array.isArray(data.data.data)) {
|
|
302
|
+
// Вложенная структура Qwen API: { success: true, data: { data: [...] } }
|
|
303
|
+
models = data.data.data;
|
|
304
|
+
logDebug(`Извлечены модели: data.data.data (${data.data.data.length} элементов)`);
|
|
305
|
+
} else if (data.data && Array.isArray(data.data)) {
|
|
306
|
+
// Простая структура: { data: [...] }
|
|
307
|
+
models = data.data;
|
|
308
|
+
logDebug(`Извлечены модели: data.data (${data.data.length} элементов)`);
|
|
309
|
+
} else if (data.models && Array.isArray(data.models)) {
|
|
310
|
+
models = data.models;
|
|
311
|
+
logDebug(`Извлечены модели: data.models (${data.models.length} элементов)`);
|
|
312
|
+
} else {
|
|
313
|
+
logWarn(`⚠️ Не удалось распознать формат ответа API. Keys: ${Object.keys(data).join(', ')}`);
|
|
314
|
+
logWarn(`Полный JSON ответ от API:`);
|
|
315
|
+
logWarn(JSON.stringify(data, null, 2));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Извлекаем ID моделей
|
|
319
|
+
const modelIds = models.map(m => {
|
|
320
|
+
if (typeof m === 'string') return m;
|
|
321
|
+
return m.id || m.model_id || m.name;
|
|
322
|
+
}).filter(Boolean);
|
|
323
|
+
|
|
324
|
+
logDebug(`Извлечено modelIds: ${modelIds.length}`);
|
|
325
|
+
if (modelIds.length > 0 && modelIds.length <= 5) {
|
|
326
|
+
logDebug(`Первые модели: ${modelIds.join(', ')}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (modelIds.length > 0) {
|
|
330
|
+
logInfo(`✅ Загружено ${modelIds.length} моделей с API`);
|
|
331
|
+
|
|
332
|
+
// Проверяем, есть ли информация о пагинации
|
|
333
|
+
let paginationInfo = null;
|
|
334
|
+
if (data.data && typeof data.data === 'object' && !Array.isArray(data.data)) {
|
|
335
|
+
const hasMore = data.data.has_more || data.data.hasMore;
|
|
336
|
+
const total = data.data.total || data.data.total_count;
|
|
337
|
+
const nextPage = data.data.next_page || data.data.nextPage;
|
|
338
|
+
|
|
339
|
+
if (hasMore || total) {
|
|
340
|
+
paginationInfo = { hasMore, total, nextPage };
|
|
341
|
+
logInfo(`📊 Пагинация: всего=${total}, есть еще=${hasMore}, следующая страница=${nextPage}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// TODO: Если есть пагинация, можно загрузить следующие страницы
|
|
346
|
+
// if (paginationInfo && paginationInfo.hasMore) {
|
|
347
|
+
// logInfo('Загрузка следующих страниц моделей...');
|
|
348
|
+
// // Здесь можно добавить цикл для загрузки всех страниц
|
|
349
|
+
// }
|
|
350
|
+
|
|
351
|
+
return modelIds;
|
|
352
|
+
} else {
|
|
353
|
+
logWarn(`⚠️ Массив models не пустой (${models.length}), но modelIds пустой`);
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
logWarn(`⚠️ API вернул статус ${response.status}, используем локальный файл`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return null;
|
|
360
|
+
} catch (error) {
|
|
361
|
+
logWarn(`❌ Не удалось загрузить модели с API: ${error.message}`);
|
|
362
|
+
logDebug(`Stack trace: ${error.stack}`);
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Загрузить список моделей из API с кэшированием
|
|
369
|
+
* Вызывается при первом запросе к LLM, результат кэшируется в памяти
|
|
370
|
+
* @returns {Promise<string[]>} - Список доступных моделей
|
|
371
|
+
*/
|
|
372
|
+
export async function ensureModelsLoaded() {
|
|
373
|
+
// Если модели уже загружены, возвращаем кэш
|
|
374
|
+
if (availableModels && availableModels.length > 0) {
|
|
375
|
+
logDebug(`📦 Модели уже загружены: ${availableModels.length} моделей, из API: ${modelsLoadedFromAPI}`);
|
|
376
|
+
return availableModels;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Если уже идет загрузка, ждем тот же промис
|
|
380
|
+
if (modelsFetchPromise) {
|
|
381
|
+
logDebug('⏳ Ожидание завершения параллельной загрузки моделей...');
|
|
382
|
+
return modelsFetchPromise;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Начинаем загрузку
|
|
386
|
+
logInfo('🚀 Начинаем процесс загрузки моделей...');
|
|
387
|
+
modelsFetchPromise = (async () => {
|
|
388
|
+
try {
|
|
389
|
+
logInfo('🔄 Загрузка списка моделей из Qwen API...');
|
|
390
|
+
const apiModels = await fetchModelsFromAPI();
|
|
391
|
+
|
|
392
|
+
logDebug(`Результат fetchModelsFromAPI: ${apiModels ? apiModels.length + ' моделей' : 'null'}`);
|
|
393
|
+
|
|
394
|
+
if (apiModels && apiModels.length > 0) {
|
|
395
|
+
availableModels = apiModels;
|
|
396
|
+
modelsLoadedFromAPI = true;
|
|
397
|
+
logInfo(`✅ Загружено ${apiModels.length} моделей с Qwen API`);
|
|
398
|
+
logInfo('===== ДОСТУПНЫЕ МОДЕЛИ (API) =====');
|
|
399
|
+
apiModels.forEach(m => logInfo(`- ${m}`));
|
|
400
|
+
logInfo('===================================');
|
|
401
|
+
|
|
402
|
+
// Устанавливаем default model как первую из API, если не задана в .env
|
|
403
|
+
if (!resolvedDefaultModel && apiModels.length > 0) {
|
|
404
|
+
resolvedDefaultModel = apiModels[0];
|
|
405
|
+
logInfo(`Default модель установлена: ${resolvedDefaultModel} (первая из API)`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return availableModels;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// API не вернул модели - используем файл
|
|
412
|
+
logWarn('⚠️ API не вернул список моделей, используем AvailableModels.txt');
|
|
413
|
+
const fileModels = loadModelsFromFile();
|
|
414
|
+
logDebug(`loadModelsFromFile вернул: ${fileModels ? fileModels.length + ' моделей' : 'null'}`);
|
|
415
|
+
return fileModels;
|
|
416
|
+
} catch (error) {
|
|
417
|
+
logError(`❌ Ошибка загрузки моделей из API`, error);
|
|
418
|
+
logWarn('⚠️ Используем резервный список из AvailableModels.txt');
|
|
419
|
+
const fileModels = loadModelsFromFile();
|
|
420
|
+
logDebug(`loadModelsFromFile (catch) вернул: ${fileModels ? fileModels.length + ' моделей' : 'null'}`);
|
|
421
|
+
return fileModels;
|
|
422
|
+
} finally {
|
|
423
|
+
modelsFetchPromise = null; // Сбрасываем промис
|
|
424
|
+
}
|
|
425
|
+
})();
|
|
426
|
+
|
|
427
|
+
return modelsFetchPromise;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Загрузить модели из файла (fallback)
|
|
432
|
+
* @returns {string[]} - Список моделей из файла
|
|
433
|
+
*/
|
|
434
|
+
function loadModelsFromFile() {
|
|
435
|
+
try {
|
|
436
|
+
logDebug(`📂 Попытка загрузки моделей из файла: ${MODELS_FILE}`);
|
|
437
|
+
|
|
438
|
+
if (!fs.existsSync(MODELS_FILE)) {
|
|
439
|
+
logError(`❌ Файл с моделями не найден: ${MODELS_FILE}`);
|
|
440
|
+
const fallback = resolvedDefaultModel ? [resolvedDefaultModel] : [getActiveModel()];
|
|
441
|
+
logDebug(`Fallback модели: ${fallback.join(', ')}`);
|
|
442
|
+
return fallback;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const fileContent = fs.readFileSync(MODELS_FILE, 'utf8');
|
|
446
|
+
logDebug(`Размер файла: ${fileContent.length} байт`);
|
|
447
|
+
|
|
448
|
+
const models = fileContent
|
|
449
|
+
.split('\n')
|
|
450
|
+
.map(l => l.trim())
|
|
451
|
+
.filter(l => l && !l.startsWith('#'));
|
|
452
|
+
|
|
453
|
+
logDebug(`Распознано моделей: ${models.length}`);
|
|
454
|
+
|
|
455
|
+
if (models.length > 0) {
|
|
456
|
+
availableModels = models;
|
|
457
|
+
modelsLoadedFromAPI = false;
|
|
458
|
+
|
|
459
|
+
logInfo('===== ДОСТУПНЫЕ МОДЕЛИ (ФАЙЛ) =====');
|
|
460
|
+
models.forEach(m => logInfo(`- ${m}`));
|
|
461
|
+
logInfo('====================================');
|
|
462
|
+
|
|
463
|
+
// Устанавливаем default model как первую из файла, если не задана в .env
|
|
464
|
+
if (!resolvedDefaultModel && models.length > 0) {
|
|
465
|
+
resolvedDefaultModel = models[0];
|
|
466
|
+
logInfo(`Default модель установлена: ${resolvedDefaultModel} (первая из файла)`);
|
|
467
|
+
}
|
|
468
|
+
} else {
|
|
469
|
+
logWarn('⚠️ Файл существует, но не содержит моделей');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return models;
|
|
473
|
+
} catch (error) {
|
|
474
|
+
logError(`❌ Ошибка при чтении файла с моделями`, error);
|
|
475
|
+
const fallback = resolvedDefaultModel ? [resolvedDefaultModel] : [getActiveModel()];
|
|
476
|
+
logDebug(`Fallback модели после ошибки: ${fallback.join(', ')}`);
|
|
477
|
+
return fallback;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Получить список доступных моделей (синхронная версия для обратной совместимости)
|
|
483
|
+
* @returns {string[]} - Список доступных моделей
|
|
484
|
+
*/
|
|
485
|
+
export function getAvailableModelsFromFile() {
|
|
486
|
+
// Если модели еще не загружены, загружаем из файла
|
|
487
|
+
if (!availableModels || availableModels.length === 0) {
|
|
488
|
+
return loadModelsFromFile();
|
|
489
|
+
}
|
|
490
|
+
return availableModels;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function getAuthKeysFromFile() {
|
|
494
|
+
try {
|
|
495
|
+
if (!fs.existsSync(AUTH_KEYS_FILE)) {
|
|
496
|
+
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# игнорируются.`;
|
|
497
|
+
try {
|
|
498
|
+
fs.writeFileSync(AUTH_KEYS_FILE, template, { encoding: 'utf8', flag: 'wx' });
|
|
499
|
+
logInfo(`Создан шаблон файла ключей: ${AUTH_KEYS_FILE}`);
|
|
500
|
+
} catch (e) {
|
|
501
|
+
logError('Не удалось создать шаблон Authorization.txt', e);
|
|
502
|
+
}
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
return fs.readFileSync(AUTH_KEYS_FILE, 'utf8')
|
|
506
|
+
.split('\n')
|
|
507
|
+
.map(l => l.trim())
|
|
508
|
+
.filter(l => l && !l.startsWith('#'));
|
|
509
|
+
} catch (error) {
|
|
510
|
+
logError('Ошибка при чтении файла с ключами авторизации', error);
|
|
511
|
+
return [];
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function isValidModel(modelName) {
|
|
516
|
+
if (!availableModels) availableModels = getAvailableModelsFromFile();
|
|
517
|
+
return availableModels.includes(modelName);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function getDefaultModel() {
|
|
521
|
+
// Используем активную модель из настроек бота
|
|
522
|
+
return getActiveModel();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function getAllModels() {
|
|
526
|
+
if (!availableModels) availableModels = getAvailableModelsFromFile();
|
|
527
|
+
return {
|
|
528
|
+
models: availableModels.map(model => ({
|
|
529
|
+
id: model,
|
|
530
|
+
name: model,
|
|
531
|
+
description: `Модель ${model}`
|
|
532
|
+
}))
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function getApiKeys() {
|
|
537
|
+
if (!authKeys) authKeys = getAuthKeysFromFile();
|
|
538
|
+
return authKeys;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ─── sendMessage — helper functions ──────────────────────────────────────────
|
|
542
|
+
|
|
543
|
+
function validateAndPrepareMessage(message) {
|
|
544
|
+
if (message === null || message === undefined) {
|
|
545
|
+
return { error: 'Сообщение не может быть пустым' };
|
|
546
|
+
}
|
|
547
|
+
if (typeof message === 'string') return { content: message };
|
|
548
|
+
if (Array.isArray(message)) {
|
|
549
|
+
const isValid = message.every(item =>
|
|
550
|
+
(item.type === 'text' && typeof item.text === 'string') ||
|
|
551
|
+
(item.type === 'image' && typeof item.image === 'string') ||
|
|
552
|
+
(item.type === 'file' && typeof item.file === 'string')
|
|
553
|
+
);
|
|
554
|
+
if (!isValid) return { error: 'Некорректная структура составного сообщения' };
|
|
555
|
+
return { content: message };
|
|
556
|
+
}
|
|
557
|
+
return { error: 'Неподдерживаемый формат сообщения' };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function resolveAuthToken(browserContext) {
|
|
561
|
+
// Сначала пытаемся получить безопасный токен (не истекающий в ближайшее время)
|
|
562
|
+
let tokenObj = await getSafeToken(TOKEN_EXPIRY_WARNING_MS);
|
|
563
|
+
|
|
564
|
+
// Если безопасных токенов нет, проверяем все токены
|
|
565
|
+
if (!tokenObj) {
|
|
566
|
+
const expiryStatus = checkAllTokensExpiry(TOKEN_EXPIRY_WARNING_MS);
|
|
567
|
+
|
|
568
|
+
if (expiryStatus.allTokensExpired) {
|
|
569
|
+
logWarn('⚠️ Все токены истекают или уже истекли!');
|
|
570
|
+
|
|
571
|
+
// Отправляем уведомление в Telegram
|
|
572
|
+
const telegramMessage = formatTokenExpiryMessage(expiryStatus.expiringTokens);
|
|
573
|
+
await sendTelegramNotification(telegramMessage);
|
|
574
|
+
|
|
575
|
+
// Пытаемся использовать любой доступный токен (даже истекающий)
|
|
576
|
+
tokenObj = await getAvailableToken();
|
|
577
|
+
|
|
578
|
+
if (!tokenObj) {
|
|
579
|
+
logError('Нет доступных токенов для использования');
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
logWarn(`Используем истекающий токен: ${tokenObj.id}`);
|
|
584
|
+
} else {
|
|
585
|
+
// Есть активные токены, но они не попали в безопасную выборку
|
|
586
|
+
tokenObj = await getAvailableToken();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (tokenObj && tokenObj.token) {
|
|
591
|
+
// Проверяем, не истекает ли токен в ближайшее время
|
|
592
|
+
const expiryInfo = checkTokenExpiry(tokenObj.id, TOKEN_EXPIRY_WARNING_MS);
|
|
593
|
+
|
|
594
|
+
if (expiryInfo.willExpireSoon) {
|
|
595
|
+
const timeLeftMs = expiryInfo.timeLeft;
|
|
596
|
+
const timeLeftMin = timeLeftMs ? Math.floor(timeLeftMs / 60000) : 0;
|
|
597
|
+
|
|
598
|
+
if (expiryInfo.isExpired || expiryInfo.isInvalid) {
|
|
599
|
+
logWarn(`Токен ${tokenObj.id} недействителен или истёк, пытаемся использовать следующий`);
|
|
600
|
+
// Помечаем как rate limited и пробуем другой
|
|
601
|
+
if (tokenObj.id !== 'browser') {
|
|
602
|
+
markRateLimited(tokenObj.id, 1); // 1 час
|
|
603
|
+
}
|
|
604
|
+
return resolveAuthToken(browserContext); // Рекурсивно пробуем другой
|
|
605
|
+
} else if (timeLeftMin <= 60) {
|
|
606
|
+
logWarn(`⚠️ Токен ${tokenObj.id} истекает через ${timeLeftMin} мин. Используем с осторожностью.`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
authToken = tokenObj.token;
|
|
611
|
+
logInfo(`Используется аккаунт: ${tokenObj.id}`);
|
|
612
|
+
return tokenObj;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (browserTokenRateLimited) {
|
|
616
|
+
logWarn('Browser-токен залимичен, пропускаем fallback');
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (!getAuthenticationStatus()) {
|
|
621
|
+
logInfo('Проверка авторизации...');
|
|
622
|
+
const authCheck = await checkAuthentication(browserContext);
|
|
623
|
+
if (!authCheck) return null;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (!authToken) {
|
|
627
|
+
logInfo('Получение токена авторизации...');
|
|
628
|
+
authToken = await extractAuthToken(browserContext);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return authToken ? { id: 'browser', token: authToken } : null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function buildPayloadV2(messageContent, model, chatId, parentId, files, systemMessage, tools, toolChoice, chatType = 't2t', size = null) {
|
|
635
|
+
const userMessageId = crypto.randomUUID();
|
|
636
|
+
const assistantChildId = crypto.randomUUID();
|
|
637
|
+
|
|
638
|
+
const isVideo = chatType === 't2v';
|
|
639
|
+
|
|
640
|
+
const featureConfig = {
|
|
641
|
+
thinking_enabled: isVideo,
|
|
642
|
+
output_schema: 'phase'
|
|
643
|
+
};
|
|
644
|
+
if (isVideo) {
|
|
645
|
+
featureConfig.research_mode = 'normal';
|
|
646
|
+
featureConfig.auto_thinking = true;
|
|
647
|
+
featureConfig.thinking_format = 'summary';
|
|
648
|
+
featureConfig.auto_search = true;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const newMessage = {
|
|
652
|
+
fid: userMessageId,
|
|
653
|
+
parentId, parent_id: parentId,
|
|
654
|
+
role: 'user',
|
|
655
|
+
content: messageContent,
|
|
656
|
+
chat_type: chatType, sub_chat_type: chatType,
|
|
657
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
658
|
+
user_action: 'chat',
|
|
659
|
+
models: [model],
|
|
660
|
+
files: files || [],
|
|
661
|
+
childrenIds: [assistantChildId],
|
|
662
|
+
extra: { meta: { subChatType: chatType } },
|
|
663
|
+
feature_config: featureConfig
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const payload = {
|
|
667
|
+
stream: !isVideo,
|
|
668
|
+
incremental_output: true,
|
|
669
|
+
chat_id: chatId,
|
|
670
|
+
chat_mode: 'normal',
|
|
671
|
+
messages: [newMessage],
|
|
672
|
+
model,
|
|
673
|
+
parent_id: parentId,
|
|
674
|
+
timestamp: Math.floor(Date.now() / 1000)
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
if (size) payload.size = size;
|
|
678
|
+
|
|
679
|
+
if (systemMessage) {
|
|
680
|
+
payload.system_message = systemMessage;
|
|
681
|
+
logDebug(`System message: ${systemMessage.substring(0, 100)}${systemMessage.length > 100 ? '...' : ''}`);
|
|
682
|
+
}
|
|
683
|
+
if (tools && Array.isArray(tools) && tools.length > 0) {
|
|
684
|
+
payload.tools = tools;
|
|
685
|
+
payload.tool_choice = toolChoice || 'auto';
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return payload;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function parseNonSseCompletionBody(body) {
|
|
692
|
+
try {
|
|
693
|
+
const parsed = JSON.parse(body);
|
|
694
|
+
const topLevelCode = parsed?.code;
|
|
695
|
+
const nestedCode = parsed?.data?.code;
|
|
696
|
+
const hasStructuredError =
|
|
697
|
+
parsed?.success === false ||
|
|
698
|
+
Boolean(parsed?.error) ||
|
|
699
|
+
Boolean(parsed?.data?.error) ||
|
|
700
|
+
Boolean(topLevelCode) ||
|
|
701
|
+
Boolean(nestedCode);
|
|
702
|
+
|
|
703
|
+
if (hasStructuredError) {
|
|
704
|
+
const isRateLimited = topLevelCode === 'RateLimited' || nestedCode === 'RateLimited';
|
|
705
|
+
return {
|
|
706
|
+
success: false,
|
|
707
|
+
status: isRateLimited ? 429 : 500,
|
|
708
|
+
errorBody: body
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (parsed.choices || parsed.id || (parsed.success === true && parsed.data)) {
|
|
713
|
+
return { success: true, isTask: false, data: parsed };
|
|
714
|
+
}
|
|
715
|
+
} catch {
|
|
716
|
+
// Ignore parse errors here and return a generic failure below.
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return { success: false, error: 'Unexpected non-SSE 200 response', errorBody: body };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function executeApiRequestWithNodeStreaming(apiUrl, payload, token, onChunk) {
|
|
723
|
+
try {
|
|
724
|
+
if (!token) return { success: false, error: 'Токен авторизации не найден' };
|
|
725
|
+
if (typeof fetch !== 'function') return { success: false, error: 'Fetch API is unavailable' };
|
|
726
|
+
|
|
727
|
+
const response = await fetchWithQwenProxy(apiUrl, {
|
|
728
|
+
method: 'POST',
|
|
729
|
+
headers: {
|
|
730
|
+
'Content-Type': 'application/json',
|
|
731
|
+
'Authorization': `Bearer ${token}`,
|
|
732
|
+
'Accept': '*/*'
|
|
733
|
+
},
|
|
734
|
+
body: JSON.stringify(payload)
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
if (!response.ok) {
|
|
738
|
+
const errorBody = await response.text();
|
|
739
|
+
return { success: false, status: response.status, statusText: response.statusText, errorBody };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (payload.stream === false) {
|
|
743
|
+
const jsonResponse = await response.json();
|
|
744
|
+
if (jsonResponse.code === 'RateLimited' || jsonResponse.error) {
|
|
745
|
+
return { success: false, status: 429, errorBody: JSON.stringify(jsonResponse) };
|
|
746
|
+
}
|
|
747
|
+
return { success: true, isTask: true, data: jsonResponse };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const contentType = response.headers.get('content-type') || '';
|
|
751
|
+
if (!contentType.includes('text/event-stream')) {
|
|
752
|
+
const body = await response.text();
|
|
753
|
+
return parseNonSseCompletionBody(body);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const reader = response.body?.getReader?.();
|
|
757
|
+
if (!reader) {
|
|
758
|
+
const body = await response.text();
|
|
759
|
+
return parseNonSseCompletionBody(body);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const decoder = new TextDecoder();
|
|
763
|
+
let buffer = '';
|
|
764
|
+
let fullContent = '';
|
|
765
|
+
let responseId = null;
|
|
766
|
+
let usage = null;
|
|
767
|
+
let finished = false;
|
|
768
|
+
let streamError = null;
|
|
769
|
+
let hasStreamedChunks = false;
|
|
770
|
+
|
|
771
|
+
while (!finished) {
|
|
772
|
+
const { done, value } = await reader.read();
|
|
773
|
+
if (done) break;
|
|
774
|
+
|
|
775
|
+
buffer += decoder.decode(value, { stream: true });
|
|
776
|
+
const lines = buffer.split('\n');
|
|
777
|
+
buffer = lines.pop() || '';
|
|
778
|
+
|
|
779
|
+
for (const rawLine of lines) {
|
|
780
|
+
const line = rawLine.trim();
|
|
781
|
+
if (!line || !line.startsWith('data:')) continue;
|
|
782
|
+
|
|
783
|
+
const jsonStr = line.substring(5).trim();
|
|
784
|
+
if (!jsonStr) continue;
|
|
785
|
+
if (jsonStr === '[DONE]') {
|
|
786
|
+
finished = true;
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
try {
|
|
791
|
+
const chunk = JSON.parse(jsonStr);
|
|
792
|
+
|
|
793
|
+
if (chunk.code === 'RateLimited' || (chunk.code && chunk.detail)) {
|
|
794
|
+
streamError = { status: 429, errorBody: JSON.stringify(chunk) };
|
|
795
|
+
finished = true;
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
if (chunk.error && !chunk.choices) {
|
|
799
|
+
streamError = { status: 500, errorBody: JSON.stringify(chunk) };
|
|
800
|
+
finished = true;
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (chunk['response.created']) responseId = chunk['response.created'].response_id;
|
|
805
|
+
if (chunk.response_id) responseId = chunk.response_id;
|
|
806
|
+
|
|
807
|
+
if (chunk.choices && chunk.choices[0]) {
|
|
808
|
+
const delta = chunk.choices[0].delta;
|
|
809
|
+
if (delta && delta.content) {
|
|
810
|
+
fullContent += delta.content;
|
|
811
|
+
if (typeof onChunk === 'function') {
|
|
812
|
+
onChunk(delta.content);
|
|
813
|
+
hasStreamedChunks = true;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (delta && delta.status === 'finished') finished = true;
|
|
817
|
+
if (chunk.choices[0].finish_reason) finished = true;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (chunk.usage) usage = chunk.usage;
|
|
821
|
+
} catch {
|
|
822
|
+
// Ignore broken chunks, keep reading stream.
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (streamError) {
|
|
828
|
+
return { success: false, ...streamError, hasStreamedChunks };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return {
|
|
832
|
+
success: true,
|
|
833
|
+
isTask: false,
|
|
834
|
+
hasStreamedChunks,
|
|
835
|
+
data: {
|
|
836
|
+
id: responseId || 'chatcmpl-' + Date.now(),
|
|
837
|
+
object: 'chat.completion',
|
|
838
|
+
created: Math.floor(Date.now() / 1000),
|
|
839
|
+
model: payload.model,
|
|
840
|
+
choices: [{ index: 0, message: { role: 'assistant', content: fullContent }, finish_reason: 'stop' }],
|
|
841
|
+
usage: usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
842
|
+
response_id: responseId
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
} catch (error) {
|
|
846
|
+
return { success: false, error: error.toString() };
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function executeApiRequest(page, apiUrl, payload, token, onChunk = null) {
|
|
851
|
+
if (payload?.stream !== false && typeof onChunk === 'function') {
|
|
852
|
+
const streamedResponse = await executeApiRequestWithNodeStreaming(apiUrl, payload, token, onChunk);
|
|
853
|
+
|
|
854
|
+
const canReturnDirectly =
|
|
855
|
+
streamedResponse.success ||
|
|
856
|
+
Boolean(streamedResponse.status) ||
|
|
857
|
+
Boolean(streamedResponse.errorBody) ||
|
|
858
|
+
streamedResponse.hasStreamedChunks === true;
|
|
859
|
+
|
|
860
|
+
if (canReturnDirectly) {
|
|
861
|
+
return streamedResponse;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
logWarn(`Node-streaming недоступен (${streamedResponse.error || 'unknown error'}), fallback к browser fetch.`);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const requestBody = { apiUrl, payload, token };
|
|
868
|
+
|
|
869
|
+
logDebug(`Используем токен: ${token ? 'Токен существует' : 'Токен отсутствует'}`);
|
|
870
|
+
logDebug(`API URL: ${apiUrl}`);
|
|
871
|
+
|
|
872
|
+
return page.evaluate(async (data) => {
|
|
873
|
+
try {
|
|
874
|
+
const t = data.token;
|
|
875
|
+
if (!t) return { success: false, error: 'Токен авторизации не найден' };
|
|
876
|
+
|
|
877
|
+
const response = await fetch(data.apiUrl, {
|
|
878
|
+
method: 'POST',
|
|
879
|
+
headers: {
|
|
880
|
+
'Content-Type': 'application/json',
|
|
881
|
+
'Authorization': `Bearer ${t}`,
|
|
882
|
+
'Accept': '*/*'
|
|
883
|
+
},
|
|
884
|
+
body: JSON.stringify(data.payload)
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
if (response.ok) {
|
|
888
|
+
if (data.payload.stream === false) {
|
|
889
|
+
const jsonResponse = await response.json();
|
|
890
|
+
if (jsonResponse.code === 'RateLimited' || jsonResponse.error) {
|
|
891
|
+
return { success: false, status: 429, errorBody: JSON.stringify(jsonResponse) };
|
|
892
|
+
}
|
|
893
|
+
return { success: true, isTask: true, data: jsonResponse };
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const contentType = response.headers.get('content-type') || '';
|
|
897
|
+
|
|
898
|
+
if (!contentType.includes('text/event-stream')) {
|
|
899
|
+
const body = await response.text();
|
|
900
|
+
try {
|
|
901
|
+
const parsed = JSON.parse(body);
|
|
902
|
+
const topLevelCode = parsed?.code;
|
|
903
|
+
const nestedCode = parsed?.data?.code;
|
|
904
|
+
const hasStructuredError =
|
|
905
|
+
parsed?.success === false ||
|
|
906
|
+
Boolean(parsed?.error) ||
|
|
907
|
+
Boolean(parsed?.data?.error) ||
|
|
908
|
+
Boolean(topLevelCode) ||
|
|
909
|
+
Boolean(nestedCode);
|
|
910
|
+
|
|
911
|
+
// API иногда возвращает JSON с success=false и code при HTTP 200.
|
|
912
|
+
if (hasStructuredError) {
|
|
913
|
+
const isRateLimited = topLevelCode === 'RateLimited' || nestedCode === 'RateLimited';
|
|
914
|
+
return {
|
|
915
|
+
success: false,
|
|
916
|
+
status: isRateLimited ? 429 : 500,
|
|
917
|
+
errorBody: body
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
// Валидный JSON-ответ completion (иногда Qwen возвращает так)
|
|
921
|
+
if (parsed.choices || parsed.id || (parsed.success === true && parsed.data)) {
|
|
922
|
+
return { success: true, isTask: false, data: parsed };
|
|
923
|
+
}
|
|
924
|
+
} catch { /* not JSON, treat as unexpected */ }
|
|
925
|
+
return { success: false, error: 'Unexpected non-SSE 200 response', errorBody: body };
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const reader = response.body.getReader();
|
|
929
|
+
const decoder = new TextDecoder();
|
|
930
|
+
let buffer = '';
|
|
931
|
+
let fullContent = '';
|
|
932
|
+
let responseId = null;
|
|
933
|
+
let usage = null;
|
|
934
|
+
let finished = false;
|
|
935
|
+
let streamError = null;
|
|
936
|
+
|
|
937
|
+
while (!finished) {
|
|
938
|
+
const { done, value } = await reader.read();
|
|
939
|
+
if (done) break;
|
|
940
|
+
buffer += decoder.decode(value, { stream: true });
|
|
941
|
+
const lines = buffer.split('\n');
|
|
942
|
+
buffer = lines.pop() || '';
|
|
943
|
+
|
|
944
|
+
for (const line of lines) {
|
|
945
|
+
if (!line.trim() || !line.startsWith('data: ')) continue;
|
|
946
|
+
const jsonStr = line.substring(6).trim();
|
|
947
|
+
if (!jsonStr) continue;
|
|
948
|
+
try {
|
|
949
|
+
const chunk = JSON.parse(jsonStr);
|
|
950
|
+
|
|
951
|
+
if (chunk.code === 'RateLimited' || (chunk.code && chunk.detail)) {
|
|
952
|
+
streamError = { status: 429, errorBody: JSON.stringify(chunk) };
|
|
953
|
+
finished = true;
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
if (chunk.error && !chunk.choices) {
|
|
957
|
+
streamError = { status: 500, errorBody: JSON.stringify(chunk) };
|
|
958
|
+
finished = true;
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (chunk['response.created']) responseId = chunk['response.created'].response_id;
|
|
963
|
+
if (chunk.choices && chunk.choices[0]) {
|
|
964
|
+
const delta = chunk.choices[0].delta;
|
|
965
|
+
if (delta && delta.content) fullContent += delta.content;
|
|
966
|
+
if (delta && delta.status === 'finished') finished = true;
|
|
967
|
+
}
|
|
968
|
+
if (chunk.usage) usage = chunk.usage;
|
|
969
|
+
} catch { /* ignore parse errors for individual chunks */ }
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (streamError) {
|
|
974
|
+
return { success: false, ...streamError };
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
return {
|
|
978
|
+
success: true,
|
|
979
|
+
isTask: false,
|
|
980
|
+
data: {
|
|
981
|
+
id: responseId || 'chatcmpl-' + Date.now(),
|
|
982
|
+
object: 'chat.completion',
|
|
983
|
+
created: Math.floor(Date.now() / 1000),
|
|
984
|
+
model: data.payload.model,
|
|
985
|
+
choices: [{ index: 0, message: { role: 'assistant', content: fullContent }, finish_reason: 'stop' }],
|
|
986
|
+
usage: usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
987
|
+
response_id: responseId
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const errorBody = await response.text();
|
|
993
|
+
return { success: false, status: response.status, statusText: response.statusText, errorBody };
|
|
994
|
+
} catch (error) {
|
|
995
|
+
return { success: false, error: error.toString() };
|
|
996
|
+
}
|
|
997
|
+
}, requestBody);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async function handleApiError(response, tokenObj, message, model, chatId, parentId, files, retryCount, chatType, size, waitForCompletion, onChunk = null) {
|
|
1001
|
+
logRaw(JSON.stringify(response));
|
|
1002
|
+
|
|
1003
|
+
// Улучшенная обработка ошибок с полной диагностикой
|
|
1004
|
+
const errorMessage = response.error || response.statusText || response.errorBody || 'Неизвестная ошибка';
|
|
1005
|
+
logError(`Ошибка при получении ответа: ${errorMessage}`);
|
|
1006
|
+
|
|
1007
|
+
// Логируем дополнительную информацию для отладки
|
|
1008
|
+
if (response.status) logDebug(`HTTP статус: ${response.status}`);
|
|
1009
|
+
if (response.errorBody) logDebug(`Тело ответа с ошибкой: ${response.errorBody.substring(0, 500)}${response.errorBody.length > 500 ? '...' : ''}`);
|
|
1010
|
+
if (response.error) logDebug(`Ошибка из response: ${response.error}`);
|
|
1011
|
+
if (response.statusText) logDebug(`StatusText: ${response.statusText}`);
|
|
1012
|
+
logDebug(`Полный объект ответа: ${JSON.stringify(response, null, 2).substring(0, 1000)}`);
|
|
1013
|
+
|
|
1014
|
+
if (response.html && response.html.includes('Verification')) {
|
|
1015
|
+
setAuthenticationStatus(false);
|
|
1016
|
+
logInfo('Обнаружена необходимость верификации, перезапуск браузера в видимом режиме...');
|
|
1017
|
+
await pagePool.clear();
|
|
1018
|
+
authToken = null;
|
|
1019
|
+
await shutdownBrowser();
|
|
1020
|
+
await initBrowser(true);
|
|
1021
|
+
return { error: 'Требуется верификация. Браузер запущен в видимом режиме.', verification: true, chatId };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (response.status === 401 || (response.errorBody && (response.errorBody.includes('Unauthorized') || response.errorBody.includes('Token has expired')))) {
|
|
1025
|
+
logWarn(`Токен ${tokenObj?.id} недействителен (401). Удаляем и пробуем другой.`);
|
|
1026
|
+
authToken = null;
|
|
1027
|
+
browserTokenRateLimited = false;
|
|
1028
|
+
if (tokenObj?.id && tokenObj.id !== 'browser') {
|
|
1029
|
+
const { markInvalid } = await import('./tokenManager.js');
|
|
1030
|
+
markInvalid(tokenObj.id);
|
|
1031
|
+
}
|
|
1032
|
+
const { hasValidTokens } = await import('./tokenManager.js');
|
|
1033
|
+
if (hasValidTokens() && retryCount < MAX_RETRY_COUNT) {
|
|
1034
|
+
return sendMessage(message, model, chatId, parentId, files, null, null, null, chatType, size, waitForCompletion, retryCount + 1, onChunk);
|
|
1035
|
+
}
|
|
1036
|
+
logError('Не осталось валидных токенов или исчерпаны попытки.');
|
|
1037
|
+
return { error: 'Все токены недействительны (401). Требуется повторная авторизация.', chatId };
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (response.status === 429 || (response.errorBody && response.errorBody.includes('RateLimited'))) {
|
|
1041
|
+
let hours = 24;
|
|
1042
|
+
try {
|
|
1043
|
+
const rateInfo = JSON.parse(response.errorBody);
|
|
1044
|
+
hours = Number(rateInfo.num) || 24;
|
|
1045
|
+
} catch { /* errorBody might not be valid JSON */ }
|
|
1046
|
+
|
|
1047
|
+
// Для генерации изображений/видео не помечаем токен как rate-limited
|
|
1048
|
+
// это отдельный лимит API, не связанный с текстовыми запросами
|
|
1049
|
+
const isMediaGeneration = chatType === 't2i' || chatType === 't2v';
|
|
1050
|
+
|
|
1051
|
+
if (isMediaGeneration) {
|
|
1052
|
+
logWarn(`⚠️ Rate limit для генерации медиа (${chatType}). НЕ помечаем токен - это отдельный лимит.`);
|
|
1053
|
+
logWarn(`⏳ Нужно подождать ${hours}ч или использовать другой аккаунт`);
|
|
1054
|
+
} else if (tokenObj?.id === 'browser') {
|
|
1055
|
+
browserTokenRateLimited = true;
|
|
1056
|
+
logWarn(`Browser-токен достиг лимита. Помечаем на ${hours}ч.`);
|
|
1057
|
+
} else if (tokenObj?.id) {
|
|
1058
|
+
markRateLimited(tokenObj.id, hours);
|
|
1059
|
+
logWarn(`Токен ${tokenObj.id} достиг лимита. Помечаем на ${hours}ч и пробуем другой токен...`);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
authToken = null;
|
|
1063
|
+
|
|
1064
|
+
// Для медиа-генерации не пытаемся retry с другим токеном
|
|
1065
|
+
// так как лимит общий для всех аккаунтов
|
|
1066
|
+
if (isMediaGeneration) {
|
|
1067
|
+
return {
|
|
1068
|
+
error: `Rate limit для генерации изображений. Попробуйте через ${hours}ч`,
|
|
1069
|
+
chatId,
|
|
1070
|
+
rateLimit: true,
|
|
1071
|
+
rateLimitHours: hours
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const { hasValidTokens } = await import('./tokenManager.js');
|
|
1076
|
+
if (hasValidTokens() && retryCount < MAX_RETRY_COUNT) {
|
|
1077
|
+
return sendMessage(message, model, chatId, parentId, files, null, null, null, chatType, size, waitForCompletion, retryCount + 1, onChunk);
|
|
1078
|
+
}
|
|
1079
|
+
return { error: `Все токены заблокированы по лимиту (${hours}ч)`, chatId };
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
return { error: response.error || response.statusText, details: response.errorBody || 'Нет дополнительных деталей', chatId };
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// ─── Main public API ─────────────────────────────────────────────────────────
|
|
1086
|
+
|
|
1087
|
+
export async function sendMessage(message, model = null, chatId = null, parentId = null, files = null, tools = null, toolChoice = null, systemMessage = null, chatType = 't2t', size = null, waitForCompletion = true, retryCount = 0, onChunk = null) {
|
|
1088
|
+
if (!availableModels) availableModels = getAvailableModelsFromFile();
|
|
1089
|
+
|
|
1090
|
+
if (!chatId) {
|
|
1091
|
+
const newChatResult = await createChatV2(model);
|
|
1092
|
+
if (newChatResult.error) {
|
|
1093
|
+
return { error: 'Не удалось создать чат: ' + newChatResult.error };
|
|
1094
|
+
};
|
|
1095
|
+
chatId = newChatResult.chatId;
|
|
1096
|
+
logInfo(`Создан новый чат v2 с ID: ${chatId}`);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const validated = validateAndPrepareMessage(message);
|
|
1100
|
+
if (validated.error) {
|
|
1101
|
+
logError(validated.error);
|
|
1102
|
+
return { error: validated.error, chatId };
|
|
1103
|
+
}
|
|
1104
|
+
const messageContent = validated.content;
|
|
1105
|
+
|
|
1106
|
+
if (!model || model.trim() === '') {
|
|
1107
|
+
logWarn('Модель не указана, используем модель по умолчанию');
|
|
1108
|
+
model = getDefaultModel();
|
|
1109
|
+
} else if (!isValidModel(model)) {
|
|
1110
|
+
logWarn(`Модель "${model}" не найдена в списке доступных.`);
|
|
1111
|
+
logWarn(`Доступные модели: ${availableModels ? availableModels.slice(0, 10).join(', ') + '...' : 'не загружены'}`);
|
|
1112
|
+
logWarn(`Используем модель по умолчанию: ${getDefaultModel()}`);
|
|
1113
|
+
model = getDefaultModel();
|
|
1114
|
+
}
|
|
1115
|
+
logInfo(`Используемая модель: "${model}"`);
|
|
1116
|
+
if (chatType !== 't2t') {
|
|
1117
|
+
const typeLabels = { t2i: 'изображение', t2v: 'видео' };
|
|
1118
|
+
logInfo(`Тип генерации: ${chatType} (${typeLabels[chatType] || chatType})${size ? `, размер: ${size}` : ''}`);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const browserContext = getBrowserContext();
|
|
1122
|
+
if (!browserContext) return { error: 'Браузер не инициализирован', chatId };
|
|
1123
|
+
|
|
1124
|
+
const tokenObj = await resolveAuthToken(browserContext);
|
|
1125
|
+
if (!tokenObj) return { error: 'Ошибка авторизации: не удалось получить токен', chatId };
|
|
1126
|
+
|
|
1127
|
+
let page = null;
|
|
1128
|
+
try {
|
|
1129
|
+
page = await pagePool.getPage(browserContext);
|
|
1130
|
+
|
|
1131
|
+
const verificationNeeded = await checkVerification(page);
|
|
1132
|
+
if (verificationNeeded) {
|
|
1133
|
+
await page.reload({ waitUntil: 'domcontentloaded', timeout: PAGE_TIMEOUT });
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (!authToken) {
|
|
1137
|
+
logWarn('Токен отсутствует перед отправкой запроса');
|
|
1138
|
+
authToken = await page.evaluate(() => localStorage.getItem('token'));
|
|
1139
|
+
if (!authToken) return { error: 'Токен авторизации не найден. Требуется перезапуск в ручном режиме.', chatId };
|
|
1140
|
+
saveAuthToken(authToken);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
logInfo('Отправка запроса к API v2...');
|
|
1144
|
+
|
|
1145
|
+
const payload = buildPayloadV2(messageContent, model, chatId, parentId, files, systemMessage, tools, toolChoice, chatType, size);
|
|
1146
|
+
logDebug('=== PAYLOAD V2 ===\n' + JSON.stringify(payload, null, 2));
|
|
1147
|
+
logDebug(`Отправка сообщения в чат ${chatId} с parent_id: ${parentId || 'null'}`);
|
|
1148
|
+
|
|
1149
|
+
const apiUrl = `${CHAT_API_URL}?chat_id=${chatId}`;
|
|
1150
|
+
const response = await executeApiRequest(page, apiUrl, payload, authToken, onChunk);
|
|
1151
|
+
|
|
1152
|
+
if (response.success && response.isTask) {
|
|
1153
|
+
logInfo('Обнаружен ответ с задачей (видеогенерация)');
|
|
1154
|
+
logRaw(JSON.stringify(response.data));
|
|
1155
|
+
|
|
1156
|
+
const taskId = extractTaskId(response.data);
|
|
1157
|
+
if (!taskId) {
|
|
1158
|
+
logError('Task ID не найден в ответе');
|
|
1159
|
+
pagePool.releasePage(page);
|
|
1160
|
+
page = null;
|
|
1161
|
+
return { error: 'Task ID not found in response', chatId, rawResponse: response.data };
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
logInfo(`Task ID: ${taskId}`);
|
|
1165
|
+
|
|
1166
|
+
if (!waitForCompletion) {
|
|
1167
|
+
logInfo('Возвращаем task_id для клиентского polling');
|
|
1168
|
+
pagePool.releasePage(page);
|
|
1169
|
+
page = null;
|
|
1170
|
+
return {
|
|
1171
|
+
id: taskId,
|
|
1172
|
+
object: 'chat.completion.task',
|
|
1173
|
+
created: Math.floor(Date.now() / 1000),
|
|
1174
|
+
model,
|
|
1175
|
+
task_id: taskId,
|
|
1176
|
+
chatId,
|
|
1177
|
+
parentId: response.data.data?.parent_id || taskId,
|
|
1178
|
+
status: 'processing',
|
|
1179
|
+
message: 'Video generation task created. Poll GET /api/tasks/status/:taskId for progress.'
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
logInfo('Начинаем polling для получения видео...');
|
|
1184
|
+
const taskResult = await pollTaskStatus(taskId, page, authToken);
|
|
1185
|
+
|
|
1186
|
+
pagePool.releasePage(page);
|
|
1187
|
+
page = null;
|
|
1188
|
+
|
|
1189
|
+
if (taskResult.success && taskResult.status === 'completed') {
|
|
1190
|
+
logInfo('Видео успешно сгенерировано');
|
|
1191
|
+
const videoUrl = extractVideoUrl(taskResult.data);
|
|
1192
|
+
return {
|
|
1193
|
+
id: taskId,
|
|
1194
|
+
object: 'chat.completion',
|
|
1195
|
+
created: Math.floor(Date.now() / 1000),
|
|
1196
|
+
model,
|
|
1197
|
+
choices: [{
|
|
1198
|
+
index: 0,
|
|
1199
|
+
message: { role: 'assistant', content: videoUrl || JSON.stringify(taskResult.data) },
|
|
1200
|
+
finish_reason: 'stop'
|
|
1201
|
+
}],
|
|
1202
|
+
usage: taskResult.data.usage || { prompt_tokens: 0, output_tokens: 0, total_tokens: 0 },
|
|
1203
|
+
response_id: taskId,
|
|
1204
|
+
chatId,
|
|
1205
|
+
parentId: taskId,
|
|
1206
|
+
task_id: taskId,
|
|
1207
|
+
video_url: videoUrl
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
logError(`Не удалось получить видео: ${taskResult.error}`);
|
|
1212
|
+
return { error: taskResult.error || 'Video generation failed', status: taskResult.status, chatId, task_id: taskId };
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
pagePool.releasePage(page);
|
|
1216
|
+
page = null;
|
|
1217
|
+
|
|
1218
|
+
if (response.success) {
|
|
1219
|
+
logRaw(JSON.stringify(response.data));
|
|
1220
|
+
logInfo('Ответ получен успешно');
|
|
1221
|
+
response.data.chatId = chatId;
|
|
1222
|
+
response.data.parentId = response.data.response_id;
|
|
1223
|
+
response.data.id = response.data.id || 'chatcmpl-' + Date.now();
|
|
1224
|
+
|
|
1225
|
+
// Fallback: если поток чанков не был отдан, отправляем контент единым куском.
|
|
1226
|
+
if (typeof onChunk === 'function' && response.data.choices?.[0]?.message?.content && !response.hasStreamedChunks) {
|
|
1227
|
+
onChunk(response.data.choices[0].message.content);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
return response.data;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return handleApiError(response, tokenObj, message, model, chatId, parentId, files, retryCount, chatType, size, waitForCompletion, onChunk);
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
logError('Ошибка при отправке сообщения', error);
|
|
1236
|
+
return { error: error.toString(), chatId };
|
|
1237
|
+
} finally {
|
|
1238
|
+
if (page) {
|
|
1239
|
+
pagePool.releasePage(page);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// ─── Task response helpers ───────────────────────────────────────────────────
|
|
1245
|
+
|
|
1246
|
+
function extractTaskId(data) {
|
|
1247
|
+
const firstMsg = data.data?.messages?.[0];
|
|
1248
|
+
if (firstMsg?.extra?.wanx?.task_id) return firstMsg.extra.wanx.task_id;
|
|
1249
|
+
return data.id || data.task_id || data.response_id || data.data?.message_id || null;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function extractVideoUrl(taskData) {
|
|
1253
|
+
if (taskData.content) return taskData.content;
|
|
1254
|
+
if (typeof taskData.result === 'string') return taskData.result;
|
|
1255
|
+
if (taskData.result?.url) return taskData.result.url;
|
|
1256
|
+
if (taskData.result?.video_url) return taskData.result.video_url;
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
export async function clearPagePool() {
|
|
1261
|
+
await pagePool.clear();
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
export function getAuthToken() {
|
|
1265
|
+
return authToken;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// ─── createChatV2 ────────────────────────────────────────────────────────────
|
|
1269
|
+
|
|
1270
|
+
export async function createChatV2(model = getDefaultModel(), title = 'Новый чат', retryCount = 0) {
|
|
1271
|
+
const browserContext = getBrowserContext();
|
|
1272
|
+
if (!browserContext) return { error: 'Браузер не инициализирован' };
|
|
1273
|
+
|
|
1274
|
+
// Используем безопасный токен
|
|
1275
|
+
const tokenObj = await getSafeToken(TOKEN_EXPIRY_WARNING_MS);
|
|
1276
|
+
if (tokenObj?.token) {
|
|
1277
|
+
authToken = tokenObj.token;
|
|
1278
|
+
logInfo(`Используется аккаунт для создания чата: ${tokenObj.id}`);
|
|
1279
|
+
|
|
1280
|
+
// Проверяем, не истекает ли токен
|
|
1281
|
+
const expiryInfo = checkTokenExpiry(tokenObj.id, TOKEN_EXPIRY_WARNING_MS);
|
|
1282
|
+
if (expiryInfo.willExpireSoon && (expiryInfo.isExpired || expiryInfo.isInvalid)) {
|
|
1283
|
+
logWarn(`Токен ${tokenObj.id} недействителен для создания чата, пробуем другой`);
|
|
1284
|
+
if (tokenObj.id !== 'browser') {
|
|
1285
|
+
markRateLimited(tokenObj.id, 1);
|
|
1286
|
+
}
|
|
1287
|
+
return createChatV2(model, title, retryCount);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (!authToken) {
|
|
1292
|
+
logInfo('Получение токена авторизации для создания чата...');
|
|
1293
|
+
authToken = await extractAuthToken(browserContext);
|
|
1294
|
+
if (!authToken) return { error: 'Не удалось получить токен авторизации' };
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
let page = null;
|
|
1298
|
+
try {
|
|
1299
|
+
page = await pagePool.getPage(browserContext);
|
|
1300
|
+
|
|
1301
|
+
const payload = { title, models: [model], chat_mode: 'normal', chat_type: 't2t', timestamp: Date.now() };
|
|
1302
|
+
const requestBody = { apiUrl: CREATE_CHAT_URL, payload, token: authToken };
|
|
1303
|
+
|
|
1304
|
+
const result = await page.evaluate(async (data) => {
|
|
1305
|
+
try {
|
|
1306
|
+
const response = await fetch(data.apiUrl, {
|
|
1307
|
+
method: 'POST',
|
|
1308
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${data.token}` },
|
|
1309
|
+
body: JSON.stringify(data.payload)
|
|
1310
|
+
});
|
|
1311
|
+
if (response.ok) return { success: true, data: await response.json() };
|
|
1312
|
+
return { success: false, status: response.status, errorBody: await response.text() };
|
|
1313
|
+
} catch (error) {
|
|
1314
|
+
return { success: false, error: error.toString() };
|
|
1315
|
+
}
|
|
1316
|
+
}, requestBody);
|
|
1317
|
+
|
|
1318
|
+
pagePool.releasePage(page);
|
|
1319
|
+
page = null;
|
|
1320
|
+
|
|
1321
|
+
if (result.success && result.data.success) {
|
|
1322
|
+
logInfo(`Чат создан: ${result.data.data.id}`);
|
|
1323
|
+
return { success: true, chatId: result.data.data.id, requestId: result.data.request_id };
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
const isTransient = result.status >= 500 && result.status < 600;
|
|
1327
|
+
if (isTransient && retryCount < MAX_RETRY_COUNT) {
|
|
1328
|
+
logWarn(`Создание чата: ${result.status}, ретрай ${retryCount + 1}/${MAX_RETRY_COUNT} через ${RETRY_DELAY}мс...`);
|
|
1329
|
+
await delay(RETRY_DELAY);
|
|
1330
|
+
return createChatV2(model, title, retryCount + 1);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const cleanError = isTransient
|
|
1334
|
+
? `Qwen API недоступен (${result.status}). Повторите позже.`
|
|
1335
|
+
: (result.errorBody || result.error || 'Неизвестная ошибка');
|
|
1336
|
+
logError(`Ошибка при создании чата: ${result.status || 'unknown'} (попытка ${retryCount + 1})`);
|
|
1337
|
+
return { error: cleanError };
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
logError('Ошибка при создании чата', error);
|
|
1340
|
+
return { error: error.toString() };
|
|
1341
|
+
} finally {
|
|
1342
|
+
if (page) {
|
|
1343
|
+
pagePool.releasePage(page);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// ─── testToken ───────────────────────────────────────────────────────────────
|
|
1349
|
+
|
|
1350
|
+
export async function testToken(token) {
|
|
1351
|
+
const browserContext = getBrowserContext();
|
|
1352
|
+
if (!browserContext) return 'ERROR';
|
|
1353
|
+
|
|
1354
|
+
let page;
|
|
1355
|
+
let shouldClosePage = false;
|
|
1356
|
+
try {
|
|
1357
|
+
page = await getPage(browserContext);
|
|
1358
|
+
shouldClosePage = page !== browserContext;
|
|
1359
|
+
await page.goto(CHAT_PAGE_URL, { waitUntil: 'domcontentloaded' });
|
|
1360
|
+
|
|
1361
|
+
const requestBody = {
|
|
1362
|
+
apiUrl: CHAT_API_URL,
|
|
1363
|
+
token,
|
|
1364
|
+
payload: { chat_type: 't2t', messages: [{ role: 'user', content: 'ping', chat_type: 't2t' }], model: DEFAULT_MODEL, stream: false }
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
const result = await page.evaluate(async (data) => {
|
|
1368
|
+
try {
|
|
1369
|
+
const res = await fetch(data.apiUrl, {
|
|
1370
|
+
method: 'POST',
|
|
1371
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${data.token}` },
|
|
1372
|
+
body: JSON.stringify(data.payload)
|
|
1373
|
+
});
|
|
1374
|
+
return { ok: res.ok, status: res.status };
|
|
1375
|
+
} catch (e) {
|
|
1376
|
+
return { ok: false, status: 0, error: e.toString() };
|
|
1377
|
+
}
|
|
1378
|
+
}, requestBody);
|
|
1379
|
+
|
|
1380
|
+
if (result.ok || result.status === 400) return 'OK';
|
|
1381
|
+
if (result.status === 401 || result.status === 403) return 'UNAUTHORIZED';
|
|
1382
|
+
if (result.status === 429) return 'RATELIMIT';
|
|
1383
|
+
return 'ERROR';
|
|
1384
|
+
} catch (e) {
|
|
1385
|
+
logError('testToken error', e);
|
|
1386
|
+
return 'ERROR';
|
|
1387
|
+
} finally {
|
|
1388
|
+
if (page) {
|
|
1389
|
+
try { if (shouldClosePage) await page.close(); } catch { }
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|