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.
@@ -0,0 +1,382 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { logError, logWarn, logInfo } from '../logger/index.js';
5
+ import { SESSION_DIR, ACCOUNTS_DIR, TOKEN_EXPIRY_WARNING_MS } from '../config.js';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ const SESSION_PATH = path.resolve(__dirname, '..', '..', SESSION_DIR);
10
+ const ACCOUNTS_PATH = path.join(SESSION_PATH, ACCOUNTS_DIR);
11
+ const TOKENS_FILE = path.join(SESSION_PATH, 'tokens.json');
12
+
13
+ let pointer = 0;
14
+
15
+ function ensureSessionDir() {
16
+ if (!fs.existsSync(SESSION_PATH)) fs.mkdirSync(SESSION_PATH, { recursive: true });
17
+ if (!fs.existsSync(ACCOUNTS_PATH)) fs.mkdirSync(ACCOUNTS_PATH, { recursive: true });
18
+ }
19
+
20
+ /**
21
+ * Проверяет наличие cookies.json для аккаунта
22
+ * @param {string} accountId - ID аккаунта
23
+ * @returns {boolean} - true если cookies.json существует
24
+ */
25
+ export function hasCookies(accountId) {
26
+ const cookiesPath = path.join(ACCOUNTS_PATH, accountId, 'cookies.json');
27
+ return fs.existsSync(cookiesPath);
28
+ }
29
+
30
+ /**
31
+ * Декодирует JWT токен и извлекает время истечения
32
+ * @param {string} token - JWT токен
33
+ * @returns {number|null} - Время истечения в миллисекундах или null
34
+ */
35
+ function decodeJwtExpiry(token) {
36
+ try {
37
+ if (!token || typeof token !== 'string') return null;
38
+
39
+ const parts = token.split('.');
40
+ if (parts.length !== 3) return null;
41
+
42
+ // Декодируем payload (вторая часть JWT)
43
+ // JWT использует URL-safe base64, нужно заменить - на + и _ на /
44
+ let base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
45
+
46
+ // Добавляем padding если нужно
47
+ while (base64.length % 4) {
48
+ base64 += '=';
49
+ }
50
+
51
+ const payload = Buffer.from(base64, 'base64').toString('utf8');
52
+ const decoded = JSON.parse(payload);
53
+
54
+ // JWT использует поле 'exp' для времени истечения (в секундах)
55
+ if (decoded.exp) {
56
+ return decoded.exp * 1000; // Конвертируем в миллисекунды
57
+ }
58
+
59
+ return null;
60
+ } catch (error) {
61
+ // Если не удалось декодировать, возвращаем null
62
+ return null;
63
+ }
64
+ }
65
+
66
+ export function loadTokens() {
67
+ ensureSessionDir();
68
+ if (!fs.existsSync(TOKENS_FILE)) return [];
69
+ try {
70
+ const tokens = JSON.parse(fs.readFileSync(TOKENS_FILE, 'utf8'));
71
+
72
+ // Добавляем expiryTime для каждого токена, если его нет
73
+ return tokens.map(token => {
74
+ if (!token.expiryTime && token.token) {
75
+ token.expiryTime = decodeJwtExpiry(token.token);
76
+ }
77
+ return token;
78
+ });
79
+ } catch (e) {
80
+ logError('TokenManager: ошибка чтения tokens.json', e);
81
+ return [];
82
+ }
83
+ }
84
+
85
+ export function saveTokens(tokens) {
86
+ ensureSessionDir();
87
+ try {
88
+ fs.writeFileSync(TOKENS_FILE, JSON.stringify(tokens, null, 2), 'utf8');
89
+ } catch (e) {
90
+ // Не логируем EACCES как ошибку - это предупреждение
91
+ if (e.code === 'EACCES') {
92
+ logWarn('⚠️ Нет прав на записи tokens.json. Запустите: sudo chown -R $USER:$USER session/');
93
+ } else {
94
+ logError('TokenManager: ошибка сохранения tokens.json', e);
95
+ }
96
+ }
97
+ }
98
+
99
+ export async function getAvailableToken() {
100
+ const tokens = loadTokens();
101
+ const now = Date.now();
102
+
103
+ // Фильтруем токены: не rate-limited, не invalid, JWT не истёк, и есть cookies
104
+ const valid = tokens.filter(t => {
105
+ // Пропускаем недействительные токены
106
+ if (t.invalid) return false;
107
+
108
+ // Пропускаем токены с rate limit в будущем
109
+ if (t.resetAt && new Date(t.resetAt).getTime() > now) return false;
110
+
111
+ // Пропускаем токены с истёкшим JWT
112
+ if (t.expiryTime && t.expiryTime <= now) return false;
113
+
114
+ // Пропускаем токены без cookies.json
115
+ if (!hasCookies(t.id)) return false;
116
+
117
+ return true;
118
+ });
119
+
120
+ if (!valid.length) return null;
121
+ const token = valid[pointer % valid.length];
122
+ pointer = (pointer + 1) % valid.length;
123
+ return token;
124
+ }
125
+
126
+ export function hasValidTokens() {
127
+ const tokens = loadTokens();
128
+ const now = Date.now();
129
+
130
+ // Проверяем, есть ли хотя бы один валидный токен с cookies
131
+ return tokens.some(t => {
132
+ // Пропускаем недействительные токены
133
+ if (t.invalid) return false;
134
+
135
+ // Пропускаем токены с rate limit в будущем
136
+ if (t.resetAt && new Date(t.resetAt).getTime() > now) return false;
137
+
138
+ // Пропускаем токены с истёкшим JWT
139
+ if (t.expiryTime && t.expiryTime <= now) return false;
140
+
141
+ // Пропускаем токены без cookies.json
142
+ if (!hasCookies(t.id)) return false;
143
+
144
+ return true;
145
+ });
146
+ }
147
+
148
+ export function markRateLimited(id, hours = 24) {
149
+ const tokens = loadTokens();
150
+ const idx = tokens.findIndex(t => t.id === id);
151
+ if (idx !== -1) {
152
+ tokens[idx].resetAt = new Date(Date.now() + hours * 3600 * 1000).toISOString();
153
+ saveTokens(tokens);
154
+ }
155
+ }
156
+
157
+ export function removeToken(id) {
158
+ saveTokens(loadTokens().filter(t => t.id !== id));
159
+ }
160
+
161
+ export { removeToken as removeInvalidToken };
162
+
163
+ export function markInvalid(id) {
164
+ const tokens = loadTokens();
165
+ const idx = tokens.findIndex(t => t.id === id);
166
+ if (idx !== -1) { tokens[idx].invalid = true; saveTokens(tokens); }
167
+ }
168
+
169
+ export function markValid(id, newToken) {
170
+ const tokens = loadTokens();
171
+ const idx = tokens.findIndex(t => t.id === id);
172
+ if (idx !== -1) {
173
+ tokens[idx].invalid = false;
174
+ tokens[idx].resetAt = null;
175
+ if (newToken) tokens[idx].token = newToken;
176
+ saveTokens(tokens);
177
+ }
178
+ }
179
+
180
+ export function listTokens() {
181
+ return loadTokens();
182
+ }
183
+
184
+ /**
185
+ * Получает только действительные токены (не истекшие, не invalid, не rate-limited, с cookies)
186
+ * @returns {Array} - Массив действительных токенов
187
+ */
188
+ export function getValidTokens() {
189
+ const tokens = loadTokens();
190
+ const now = Date.now();
191
+
192
+ return tokens.filter(t => {
193
+ // Пропускаем недействительные токены
194
+ if (t.invalid) return false;
195
+
196
+ // Пропускаем токены с rate limit в будущем
197
+ if (t.resetAt && new Date(t.resetAt).getTime() > now) return false;
198
+
199
+ // Пропускаем токены с истёкшим JWT
200
+ if (t.expiryTime && t.expiryTime <= now) return false;
201
+
202
+ // Пропускаем токены без cookies.json
203
+ if (!hasCookies(t.id)) return false;
204
+
205
+ return true;
206
+ });
207
+ }
208
+
209
+ /**
210
+ * Проверяет, истекает ли токен в ближайшее время
211
+ * Проверяет оба параметра: resetAt (rate limit) и expiryTime (JWT expiry)
212
+ * @param {string} tokenId - ID токена
213
+ * @param {number} warningMs - Время предупреждения в мс (по умолчанию 1 час)
214
+ * @returns {object} - {willExpireSoon: boolean, expiresAt: Date|null, timeLeft: number|null}
215
+ */
216
+ export function checkTokenExpiry(tokenId, warningMs = TOKEN_EXPIRY_WARNING_MS) {
217
+ const tokens = loadTokens();
218
+ const token = tokens.find(t => t.id === tokenId);
219
+
220
+ if (!token) {
221
+ return { willExpireSoon: false, expiresAt: null, timeLeft: null, tokenFound: false };
222
+ }
223
+
224
+ const now = Date.now();
225
+
226
+ // Если токен помечен как недействительный
227
+ if (token.invalid) {
228
+ return { willExpireSoon: true, expiresAt: null, timeLeft: null, tokenFound: true, isInvalid: true };
229
+ }
230
+
231
+ // Проверяем JWT expiry time (если есть)
232
+ if (token.expiryTime) {
233
+ const jwtTimeLeft = token.expiryTime - now;
234
+
235
+ // Если JWT уже истёк
236
+ if (jwtTimeLeft <= 0) {
237
+ return {
238
+ willExpireSoon: true,
239
+ expiresAt: new Date(token.expiryTime),
240
+ timeLeft: 0,
241
+ tokenFound: true,
242
+ isExpired: true,
243
+ expiredType: 'jwt'
244
+ };
245
+ }
246
+
247
+ // Если JWT истекает в ближайшее время
248
+ if (jwtTimeLeft <= warningMs) {
249
+ return {
250
+ willExpireSoon: true,
251
+ expiresAt: new Date(token.expiryTime),
252
+ timeLeft: jwtTimeLeft,
253
+ tokenFound: true,
254
+ isExpiringSoon: true,
255
+ expiredType: 'jwt'
256
+ };
257
+ }
258
+ }
259
+
260
+ // Если есть время сброса лимита
261
+ if (token.resetAt) {
262
+ const resetTime = new Date(token.resetAt).getTime();
263
+ const timeLeft = resetTime - now;
264
+
265
+ // Если уже истёк или истекает в ближайшее время
266
+ if (timeLeft <= 0) {
267
+ return {
268
+ willExpireSoon: true,
269
+ expiresAt: new Date(token.resetAt),
270
+ timeLeft: 0,
271
+ tokenFound: true,
272
+ isExpired: true,
273
+ expiredType: 'rate_limit'
274
+ };
275
+ }
276
+
277
+ if (timeLeft <= warningMs) {
278
+ return {
279
+ willExpireSoon: true,
280
+ expiresAt: new Date(token.resetAt),
281
+ timeLeft,
282
+ tokenFound: true,
283
+ isExpiringSoon: true,
284
+ expiredType: 'rate_limit'
285
+ };
286
+ }
287
+
288
+ return {
289
+ willExpireSoon: false,
290
+ expiresAt: new Date(token.resetAt),
291
+ timeLeft,
292
+ tokenFound: true
293
+ };
294
+ }
295
+
296
+ // Если нет времени сброса, токен активен
297
+ return { willExpireSoon: false, expiresAt: null, timeLeft: null, tokenFound: true };
298
+ }
299
+
300
+ /**
301
+ * Проверяет все токены и возвращает информацию об истекающих
302
+ * @param {number} warningMs - Время предупреждения в мс
303
+ * @returns {object} - {expiringTokens: Array, allTokensExpired: boolean, totalTokens: number}
304
+ */
305
+ export function checkAllTokensExpiry(warningMs = TOKEN_EXPIRY_WARNING_MS) {
306
+ const tokens = loadTokens();
307
+ const now = Date.now();
308
+
309
+ const expiringTokens = [];
310
+ let activeTokens = 0;
311
+
312
+ tokens.forEach(token => {
313
+ const expiryInfo = checkTokenExpiry(token.id, warningMs);
314
+
315
+ if (expiryInfo.willExpireSoon) {
316
+ expiringTokens.push({
317
+ ...token,
318
+ expiryInfo
319
+ });
320
+ } else {
321
+ activeTokens++;
322
+ }
323
+ });
324
+
325
+ return {
326
+ expiringTokens,
327
+ allTokensExpired: activeTokens === 0,
328
+ totalTokens: tokens.length,
329
+ activeTokens
330
+ };
331
+ }
332
+
333
+ /**
334
+ * Получает токен, который не истекает в ближайшее время
335
+ * Проверяет оба параметра: resetAt (rate limit) и expiryTime (JWT expiry)
336
+ * Требует наличия cookies.json
337
+ * @param {number} warningMs - Время предупреждения в мс
338
+ * @returns {object|null} - Токен или null
339
+ */
340
+ export async function getSafeToken(warningMs = TOKEN_EXPIRY_WARNING_MS) {
341
+ const tokens = loadTokens();
342
+ const now = Date.now();
343
+
344
+ // Фильтруем токены, которые не истекают в ближайшее время и имеют cookies
345
+ const safeTokens = tokens.filter(t => {
346
+ // Пропускаем недействительные токены
347
+ if (t.invalid) return false;
348
+
349
+ // Пропускаем токены без cookies.json
350
+ if (!hasCookies(t.id)) return false;
351
+
352
+ // Проверяем rate limit reset time
353
+ if (t.resetAt) {
354
+ const resetTime = new Date(t.resetAt).getTime();
355
+ // Если reset time в будущем и меньше warningMs - токен небезопасен
356
+ if (resetTime > now && (resetTime - now) <= warningMs) {
357
+ return false;
358
+ }
359
+ }
360
+
361
+ // Проверяем JWT expiry time (если есть)
362
+ if (t.expiryTime) {
363
+ const jwtTimeLeft = t.expiryTime - now;
364
+ // Если JWT истекает в ближайшее время - токен небезопасен
365
+ if (jwtTimeLeft <= warningMs) {
366
+ return false;
367
+ }
368
+ }
369
+
370
+ return true;
371
+ });
372
+
373
+ if (safeTokens.length === 0) {
374
+ return null;
375
+ }
376
+
377
+ const token = safeTokens[pointer % safeTokens.length];
378
+ pointer = (pointer + 1) % safeTokens.length;
379
+
380
+ logInfo(`Использован безопасный токен: ${token.id}`);
381
+ return token;
382
+ }
@@ -0,0 +1,171 @@
1
+ import { saveSession } from './session.js';
2
+ import { setAuthenticationStatus, getAuthenticationStatus, restartBrowserInHeadlessMode } from './browser.js';
3
+ import { extractAuthToken } from '../api/chat.js';
4
+ import { logInfo, logError, logWarn } from '../logger/index.js';
5
+ import { CHAT_PAGE_URL, AUTH_SIGNIN_URL, PAGE_TIMEOUT, RETRY_DELAY } from '../config.js';
6
+
7
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
8
+
9
+ function isPlaywright(context) {
10
+ return context && typeof context.newPage === 'function';
11
+ }
12
+
13
+ async function getPage(context) {
14
+ if (context && typeof context.goto === 'function') return context;
15
+ if (context && typeof context.newPage === 'function') return await context.newPage();
16
+ throw new Error('Неверный контекст: не страница Puppeteer, не контекст Playwright');
17
+ }
18
+
19
+ async function promptUser(question) {
20
+ return new Promise(resolve => {
21
+ process.stdout.write(question);
22
+ const onData = (data) => {
23
+ process.stdin.removeListener('data', onData);
24
+ process.stdin.pause();
25
+ resolve(data.toString().trim());
26
+ };
27
+ process.stdin.resume();
28
+ process.stdin.once('data', onData);
29
+ });
30
+ }
31
+
32
+ async function countLoginContainers(page, isPW) {
33
+ if (isPW) return page.locator('.login-container').count();
34
+ return (await page.$$('.login-container')).length;
35
+ }
36
+
37
+ export async function checkAuthentication(context) {
38
+ try {
39
+ if (getAuthenticationStatus()) return true;
40
+
41
+ const page = await getPage(context);
42
+ const isPW = isPlaywright(context);
43
+
44
+ logInfo('Проверка авторизации...');
45
+
46
+ try {
47
+ await page.goto(CHAT_PAGE_URL, { waitUntil: 'domcontentloaded', timeout: PAGE_TIMEOUT });
48
+ if (isPW) await page.waitForLoadState('domcontentloaded');
49
+ await delay(RETRY_DELAY);
50
+
51
+ const pageTitle = await page.title();
52
+ if (pageTitle.includes('Verification')) {
53
+ logWarn('Обнаружена страница верификации. Пожалуйста, пройдите верификацию вручную.');
54
+ await promptUser('После прохождения верификации нажмите ENTER для продолжения...');
55
+ logInfo('Верификация подтверждена пользователем.');
56
+ }
57
+
58
+ const loginCount = await countLoginContainers(page, isPW);
59
+
60
+ if (loginCount === 0) {
61
+ logInfo('Авторизация обнаружена');
62
+ setAuthenticationStatus(true);
63
+ try {
64
+ await extractAuthToken(context, true);
65
+ await saveSession(context);
66
+ logInfo('Сессия обновлена');
67
+ } catch (e) { logError('Не удалось обновить сессию', e); }
68
+ if (isPW) await page.close();
69
+ return true;
70
+ }
71
+
72
+ console.log('------------------------------------------------------');
73
+ console.log(' НЕОБХОДИМА АВТОРИЗАЦИЯ');
74
+ console.log('------------------------------------------------------');
75
+ console.log('1. Войдите в систему через GitHub или другой способ в открытом браузере');
76
+ console.log('2. Дождитесь завершения процесса авторизации');
77
+ console.log('3. Нажмите ENTER в этой консоли');
78
+ console.log('------------------------------------------------------');
79
+
80
+ await promptUser('После успешной авторизации нажмите ENTER для продолжения...');
81
+ logInfo('Пользователь подтвердил завершение авторизации.');
82
+
83
+ await page.reload({ waitUntil: 'domcontentloaded', timeout: PAGE_TIMEOUT });
84
+ await delay(3000);
85
+
86
+ const loginCountAfter = await countLoginContainers(page, isPW);
87
+
88
+ if (loginCountAfter === 0) {
89
+ logInfo('Авторизация подтверждена.');
90
+ setAuthenticationStatus(true);
91
+ await saveSession(context);
92
+ await extractAuthToken(context, true);
93
+ if (isPW) await page.close();
94
+ return true;
95
+ }
96
+
97
+ logWarn('Авторизация не обнаружена.');
98
+ setAuthenticationStatus(false);
99
+ return false;
100
+ } catch (error) {
101
+ if (isPW) await page.close().catch(() => {});
102
+ throw error;
103
+ }
104
+ } catch (error) {
105
+ logError('Ошибка при проверке авторизации', error);
106
+ setAuthenticationStatus(false);
107
+ return false;
108
+ }
109
+ }
110
+
111
+ export async function startManualAuthentication(context, skipRestart = false) {
112
+ try {
113
+ const page = await getPage(context);
114
+ const isPW = isPlaywright(context);
115
+
116
+ logInfo('Открытие страницы для ручной авторизации...');
117
+
118
+ try {
119
+ await page.goto(AUTH_SIGNIN_URL, { waitUntil: 'load', timeout: PAGE_TIMEOUT });
120
+
121
+ console.log('------------------------------------------------------');
122
+ console.log(' НЕОБХОДИМА АВТОРИЗАЦИЯ');
123
+ console.log('------------------------------------------------------');
124
+ console.log('1. Войдите в систему в открытом браузере');
125
+ console.log('2. Дождитесь завершения процесса авторизации');
126
+ console.log('3. Нажмите ENTER в этой консоли');
127
+ console.log('------------------------------------------------------');
128
+
129
+ await promptUser('После успешной авторизации нажмите ENTER для продолжения...');
130
+
131
+ await page.goto(CHAT_PAGE_URL, { waitUntil: 'domcontentloaded', timeout: PAGE_TIMEOUT });
132
+ await delay(RETRY_DELAY);
133
+
134
+ const loginCount = await countLoginContainers(page, isPW);
135
+
136
+ if (loginCount === 0) {
137
+ logInfo('Авторизация подтверждена.');
138
+ setAuthenticationStatus(true);
139
+ await saveSession(context);
140
+ await extractAuthToken(context, true);
141
+ logInfo('Сессия сохранена успешно!');
142
+ if (isPW) await page.close();
143
+ if (!skipRestart) await restartBrowserInHeadlessMode();
144
+ return true;
145
+ }
146
+
147
+ logWarn('Авторизация не удалась.');
148
+ setAuthenticationStatus(false);
149
+ return false;
150
+ } catch (error) {
151
+ if (isPW) await page.close().catch(() => {});
152
+ throw error;
153
+ }
154
+ } catch (error) {
155
+ logError('Ошибка при ручной авторизации', error);
156
+ setAuthenticationStatus(false);
157
+ return false;
158
+ }
159
+ }
160
+
161
+ export async function checkVerification(page) {
162
+ try {
163
+ const pageTitle = await page.title();
164
+ if (pageTitle.includes('Verification')) {
165
+ logWarn('Обнаружена страница верификации');
166
+ await promptUser('Пройдите верификацию и нажмите ENTER...');
167
+ return true;
168
+ }
169
+ return false;
170
+ } catch { return false; }
171
+ }