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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import puppeteer from 'puppeteer-extra';
|
|
2
|
+
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
|
3
|
+
import { saveSession, saveAuthToken } from './session.js';
|
|
4
|
+
import { startManualAuthentication } from './auth.js';
|
|
5
|
+
import { clearPagePool, getAuthToken } from '../api/chat.js';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { logInfo, logError, logWarn, logDebug } from '../logger/index.js';
|
|
9
|
+
import {
|
|
10
|
+
CHAT_PAGE_URL, NAVIGATION_TIMEOUT, RETRY_DELAY,
|
|
11
|
+
VIEWPORT_WIDTH, VIEWPORT_HEIGHT, USER_AGENT,
|
|
12
|
+
SESSION_DIR, ACCOUNTS_DIR
|
|
13
|
+
} from '../config.js';
|
|
14
|
+
|
|
15
|
+
puppeteer.use(StealthPlugin());
|
|
16
|
+
|
|
17
|
+
let browserInstance = null;
|
|
18
|
+
let browserContext = null;
|
|
19
|
+
export let isAuthenticated = false;
|
|
20
|
+
|
|
21
|
+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
22
|
+
|
|
23
|
+
export async function initBrowser(visibleMode = true, skipManualRestart = false) {
|
|
24
|
+
if (browserInstance) return true;
|
|
25
|
+
|
|
26
|
+
logInfo('Инициализация браузера с Puppeteer Stealth...');
|
|
27
|
+
try {
|
|
28
|
+
browserInstance = await puppeteer.launch({
|
|
29
|
+
headless: !visibleMode,
|
|
30
|
+
slowMo: visibleMode ? 30 : 0,
|
|
31
|
+
executablePath: process.env.CHROME_PATH || undefined,
|
|
32
|
+
args: [
|
|
33
|
+
'--no-sandbox', '--disable-setuid-sandbox',
|
|
34
|
+
'--disable-blink-features=AutomationControlled',
|
|
35
|
+
'--disable-dev-shm-usage', '--disable-web-security',
|
|
36
|
+
'--disable-features=IsolateOrigins,site-per-process',
|
|
37
|
+
`--window-size=${VIEWPORT_WIDTH},${VIEWPORT_HEIGHT}`,
|
|
38
|
+
'--start-maximized', '--disable-infobars',
|
|
39
|
+
'--disable-extensions', '--disable-gpu',
|
|
40
|
+
'--no-first-run', '--no-default-browser-check',
|
|
41
|
+
'--ignore-certificate-errors', '--ignore-certificate-errors-spki-list'
|
|
42
|
+
],
|
|
43
|
+
defaultViewport: { width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT },
|
|
44
|
+
ignoreHTTPSErrors: true
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const pages = await browserInstance.pages();
|
|
48
|
+
const page = pages.length > 0 ? pages[0] : await browserInstance.newPage();
|
|
49
|
+
|
|
50
|
+
await page.setUserAgent(USER_AGENT);
|
|
51
|
+
await page.setViewport({ width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT, deviceScaleFactor: 1 });
|
|
52
|
+
await page.setExtraHTTPHeaders({
|
|
53
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
54
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
55
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
|
56
|
+
'Connection': 'keep-alive',
|
|
57
|
+
'Upgrade-Insecure-Requests': '1'
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await page.evaluateOnNewDocument(() => {
|
|
61
|
+
Object.defineProperty(navigator, 'platform', { get: () => 'Win32' });
|
|
62
|
+
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
|
|
63
|
+
Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
|
|
64
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
65
|
+
get: () => [{ 0: { type: 'application/x-google-chrome-pdf', suffixes: 'pdf', description: 'Portable Document Format' }, description: 'Portable Document Format', filename: 'internal-pdf-viewer', length: 1, name: 'Chrome PDF Plugin' }]
|
|
66
|
+
});
|
|
67
|
+
Object.defineProperty(navigator, 'connection', {
|
|
68
|
+
get: () => ({ effectiveType: '4g', rtt: 50, downlink: 10, saveData: false })
|
|
69
|
+
});
|
|
70
|
+
if (!navigator.getBattery) {
|
|
71
|
+
navigator.getBattery = () => Promise.resolve({ charging: true, chargingTime: 0, dischargingTime: Infinity, level: 1 });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const originalAddEventListener = EventTarget.prototype.addEventListener;
|
|
75
|
+
EventTarget.prototype.addEventListener = function (type, listener, options) {
|
|
76
|
+
if (type === 'mousemove' || type === 'mousedown' || type === 'mouseup') {
|
|
77
|
+
const wrappedListener = function (event) { setTimeout(() => listener.call(this, event), Math.random() * 3); };
|
|
78
|
+
return originalAddEventListener.call(this, type, wrappedListener, options);
|
|
79
|
+
}
|
|
80
|
+
return originalAddEventListener.call(this, type, listener, options);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
|
84
|
+
HTMLCanvasElement.prototype.toDataURL = function (type) {
|
|
85
|
+
const context = this.getContext('2d');
|
|
86
|
+
if (context) {
|
|
87
|
+
const imageData = context.getImageData(0, 0, this.width, this.height);
|
|
88
|
+
const data = imageData.data;
|
|
89
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
90
|
+
const noise = Math.floor(Math.random() * 5) - 2;
|
|
91
|
+
data[i] = Math.max(0, Math.min(255, data[i] + noise));
|
|
92
|
+
data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + noise));
|
|
93
|
+
data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + noise));
|
|
94
|
+
}
|
|
95
|
+
context.putImageData(imageData, 0, 0);
|
|
96
|
+
}
|
|
97
|
+
return originalToDataURL.apply(this, arguments);
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
browserContext = page;
|
|
102
|
+
logInfo('Браузер инициализирован с максимальной защитой от обнаружения');
|
|
103
|
+
|
|
104
|
+
if (visibleMode) {
|
|
105
|
+
await startManualAuthenticationPuppeteer(page, skipManualRestart);
|
|
106
|
+
}
|
|
107
|
+
// loadSessionPuppeteer removed — was dead code (always returned false)
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
logError('Ошибка при инициализации браузера', error);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function saveSessionPuppeteer(page) {
|
|
117
|
+
try {
|
|
118
|
+
const cookies = await page.cookies();
|
|
119
|
+
const sessionDir = path.join(process.cwd(), SESSION_DIR, ACCOUNTS_DIR);
|
|
120
|
+
if (!fs.existsSync(sessionDir)) fs.mkdirSync(sessionDir, { recursive: true });
|
|
121
|
+
|
|
122
|
+
const accountId = `acc_${Date.now()}`;
|
|
123
|
+
const accountDir = path.join(sessionDir, accountId);
|
|
124
|
+
if (!fs.existsSync(accountDir)) fs.mkdirSync(accountDir, { recursive: true });
|
|
125
|
+
|
|
126
|
+
fs.writeFileSync(path.join(accountDir, 'cookies.json'), JSON.stringify(cookies, null, 2));
|
|
127
|
+
logInfo(`Cookies сохранены для аккаунта ${accountId}`);
|
|
128
|
+
return accountId;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
logError('Ошибка при сохранении сессии', error);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function startManualAuthenticationPuppeteer(page, skipManualRestart) {
|
|
136
|
+
try {
|
|
137
|
+
logInfo('Открытие страницы для ручной авторизации...');
|
|
138
|
+
await page.goto(CHAT_PAGE_URL, { waitUntil: 'networkidle2', timeout: NAVIGATION_TIMEOUT });
|
|
139
|
+
await delay(5000);
|
|
140
|
+
|
|
141
|
+
console.log('------------------------------------------------------');
|
|
142
|
+
console.log(' НЕОБХОДИМА АВТОРИЗАЦИЯ');
|
|
143
|
+
console.log('------------------------------------------------------');
|
|
144
|
+
console.log('Пожалуйста, выполните следующие действия:');
|
|
145
|
+
console.log('1. Войдите в систему в открытом браузере');
|
|
146
|
+
console.log('2. ВАЖНО: Двигайте мышью естественно, не спешите');
|
|
147
|
+
console.log('3. Если появится слайдер капчи - решите её медленно');
|
|
148
|
+
console.log('4. Дождитесь полной загрузки главной страницы');
|
|
149
|
+
console.log('5. После успешной авторизации нажмите ENTER в консоли');
|
|
150
|
+
console.log('------------------------------------------------------');
|
|
151
|
+
console.log('После успешной авторизации нажмите ENTER для продолжения...');
|
|
152
|
+
|
|
153
|
+
await new Promise((resolve) => {
|
|
154
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
155
|
+
process.stdin.resume();
|
|
156
|
+
process.stdin.setEncoding('utf8');
|
|
157
|
+
const onData = (key) => {
|
|
158
|
+
if (key === '\n' || key === '\r' || key.charCodeAt(0) === 13) {
|
|
159
|
+
process.stdin.pause();
|
|
160
|
+
process.stdin.removeListener('data', onData);
|
|
161
|
+
logInfo('Получено подтверждение, продолжаем...');
|
|
162
|
+
resolve();
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
process.stdin.on('data', onData);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const cookies = await page.cookies();
|
|
169
|
+
logInfo(`Сохранено ${cookies.length} cookies`);
|
|
170
|
+
|
|
171
|
+
const token = await page.evaluate(() =>
|
|
172
|
+
localStorage.getItem('token') || localStorage.getItem('auth_token') ||
|
|
173
|
+
localStorage.getItem('access_token') || sessionStorage.getItem('token') ||
|
|
174
|
+
sessionStorage.getItem('auth_token') || null
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (token) {
|
|
178
|
+
logInfo('Токен найден и будет сохранен');
|
|
179
|
+
saveAuthToken(token);
|
|
180
|
+
} else {
|
|
181
|
+
logWarn('Токен не найден в localStorage/sessionStorage');
|
|
182
|
+
logInfo('Попытка извлечь токен из cookies...');
|
|
183
|
+
const tokenCookie = cookies.find(c => c.name.toLowerCase().includes('token') || c.name.toLowerCase().includes('auth'));
|
|
184
|
+
if (tokenCookie) {
|
|
185
|
+
logInfo(`Токен найден в cookie: ${tokenCookie.name}`);
|
|
186
|
+
saveAuthToken(tokenCookie.value);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const accountId = await saveSessionPuppeteer(page);
|
|
191
|
+
if (accountId) logInfo(`Сессия сохранена с ID: ${accountId}`);
|
|
192
|
+
|
|
193
|
+
setAuthenticationStatus(true);
|
|
194
|
+
logInfo('Авторизация завершена успешно');
|
|
195
|
+
|
|
196
|
+
if (!skipManualRestart) await restartBrowserInHeadlessMode();
|
|
197
|
+
} catch (error) {
|
|
198
|
+
logError('Ошибка при ручной авторизации', error);
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function restartBrowserInHeadlessMode() {
|
|
204
|
+
logInfo('Перезапуск браузера в фоновом режиме...');
|
|
205
|
+
const token = getAuthToken();
|
|
206
|
+
if (token) { logDebug('Сохранение токена...'); saveAuthToken(token); await delay(1000); }
|
|
207
|
+
await shutdownBrowser();
|
|
208
|
+
await delay(RETRY_DELAY);
|
|
209
|
+
const success = await initBrowser(false);
|
|
210
|
+
logInfo(success ? 'Браузер перезапущен в фоновом режиме' : 'Ошибка при перезапуске браузера');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function shutdownBrowser() {
|
|
214
|
+
try {
|
|
215
|
+
try { await clearPagePool(); } catch (e) { logError('Ошибка при очистке пула страниц', e); }
|
|
216
|
+
if (browserInstance) {
|
|
217
|
+
try {
|
|
218
|
+
const pages = await browserInstance.pages();
|
|
219
|
+
for (const page of pages) await page.close().catch(() => {});
|
|
220
|
+
await browserInstance.close();
|
|
221
|
+
} catch (e) { logError('Ошибка при закрытии браузера', e); }
|
|
222
|
+
}
|
|
223
|
+
browserContext = null;
|
|
224
|
+
browserInstance = null;
|
|
225
|
+
logInfo('Браузер закрыт');
|
|
226
|
+
} catch (error) {
|
|
227
|
+
logError('Ошибка при завершении работы браузера', error);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function getBrowserContext() { return browserContext; }
|
|
232
|
+
export function setAuthenticationStatus(status) { isAuthenticated = status; }
|
|
233
|
+
export function getAuthenticationStatus() { return isAuthenticated; }
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { logInfo, logError, logWarn } from '../logger/index.js';
|
|
5
|
+
import { SESSION_DIR } 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 TOKEN_FILE = path.join(SESSION_PATH, 'auth_token.txt');
|
|
11
|
+
|
|
12
|
+
function getSessionFilePath(accountId, fileName) {
|
|
13
|
+
return accountId
|
|
14
|
+
? path.join(SESSION_PATH, 'accounts', accountId, fileName)
|
|
15
|
+
: path.join(SESSION_PATH, fileName);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ensureDir(dirPath) {
|
|
19
|
+
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function initSessionDirectory() {
|
|
23
|
+
ensureDir(SESSION_PATH);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function saveSession(context, accountId = null) {
|
|
27
|
+
try {
|
|
28
|
+
initSessionDirectory();
|
|
29
|
+
const isPuppeteer = context && typeof context.goto === 'function';
|
|
30
|
+
const isPlaywright = context && typeof context.storageState === 'function';
|
|
31
|
+
|
|
32
|
+
if (isPuppeteer) {
|
|
33
|
+
const cookies = await context.cookies();
|
|
34
|
+
const sessionPath = getSessionFilePath(accountId, 'cookies.json');
|
|
35
|
+
ensureDir(path.dirname(sessionPath));
|
|
36
|
+
fs.writeFileSync(sessionPath, JSON.stringify(cookies, null, 2));
|
|
37
|
+
logInfo('Сессия Puppeteer сохранена');
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isPlaywright && context.browser()) {
|
|
42
|
+
const sessionPath = getSessionFilePath(accountId, 'state.json');
|
|
43
|
+
ensureDir(path.dirname(sessionPath));
|
|
44
|
+
await context.storageState({ path: sessionPath });
|
|
45
|
+
logInfo('Сессия Playwright сохранена');
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
logError('Неизвестный тип контекста браузера');
|
|
50
|
+
return false;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
logError('Ошибка при сохранении сессии', error);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function loadSession(context, accountId = null) {
|
|
58
|
+
try {
|
|
59
|
+
const isPuppeteer = context && typeof context.goto === 'function';
|
|
60
|
+
const isPlaywright = context && typeof context.storageState === 'function';
|
|
61
|
+
|
|
62
|
+
if (isPuppeteer) {
|
|
63
|
+
const sessionPath = getSessionFilePath(accountId, 'cookies.json');
|
|
64
|
+
if (fs.existsSync(sessionPath)) {
|
|
65
|
+
const cookies = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
|
|
66
|
+
await context.setCookie(...cookies);
|
|
67
|
+
logInfo('Сессия Puppeteer загружена');
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
} else if (isPlaywright) {
|
|
71
|
+
const sessionPath = getSessionFilePath(accountId, 'state.json');
|
|
72
|
+
if (fs.existsSync(sessionPath)) {
|
|
73
|
+
await context.storageState({ path: sessionPath });
|
|
74
|
+
logInfo('Сессия Playwright загружена');
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logError('Ошибка при загрузке сессии', error);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function clearSession(accountId = null) {
|
|
85
|
+
try {
|
|
86
|
+
const paths = [
|
|
87
|
+
getSessionFilePath(accountId, 'state.json'),
|
|
88
|
+
getSessionFilePath(accountId, 'cookies.json')
|
|
89
|
+
];
|
|
90
|
+
let cleared = false;
|
|
91
|
+
for (const p of paths) {
|
|
92
|
+
if (fs.existsSync(p)) { fs.unlinkSync(p); cleared = true; }
|
|
93
|
+
}
|
|
94
|
+
if (cleared) logInfo('Сессия очищена');
|
|
95
|
+
return cleared;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logError('Ошибка при очистке сессии', error);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function hasSession(accountId = null) {
|
|
103
|
+
return [
|
|
104
|
+
getSessionFilePath(accountId, 'state.json'),
|
|
105
|
+
getSessionFilePath(accountId, 'cookies.json')
|
|
106
|
+
].some(p => fs.existsSync(p));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function saveAuthToken(token) {
|
|
110
|
+
try {
|
|
111
|
+
initSessionDirectory();
|
|
112
|
+
if (token) {
|
|
113
|
+
fs.writeFileSync(TOKEN_FILE, token, 'utf8');
|
|
114
|
+
logInfo('Токен авторизации сохранен');
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logError('Ошибка при сохранении токена авторизации', error);
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function loadAuthToken() {
|
|
124
|
+
try {
|
|
125
|
+
if (fs.existsSync(TOKEN_FILE)) {
|
|
126
|
+
const token = fs.readFileSync(TOKEN_FILE, 'utf8');
|
|
127
|
+
logInfo('Токен авторизации загружен');
|
|
128
|
+
return token;
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
logError('Ошибка при загрузке токена авторизации', error);
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
|
|
6
|
+
// Determine if running as global CLI or local development
|
|
7
|
+
const isGlobalInstall = process.env.QWEN_API_PROXY_GLOBAL === 'true';
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
// Use current working directory for global install, package root for development
|
|
12
|
+
const BASE_DIR = isGlobalInstall ? process.cwd() : PACKAGE_ROOT;
|
|
13
|
+
|
|
14
|
+
// Load .env file from working directory
|
|
15
|
+
const envPath = path.join(BASE_DIR, '.env');
|
|
16
|
+
const dotenvResult = dotenv.config({ path: envPath });
|
|
17
|
+
if (dotenvResult.error) {
|
|
18
|
+
console.warn('⚠️ .env file not found, using environment variables');
|
|
19
|
+
} else {
|
|
20
|
+
console.log('✅ .env file loaded');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// config.js — Единый источник конфигурации проекта.
|
|
24
|
+
// Все значения читаются из env-переменных с фоллбэками на дефолты.
|
|
25
|
+
|
|
26
|
+
function toBoolean(value) {
|
|
27
|
+
if (typeof value === 'boolean') return value;
|
|
28
|
+
if (typeof value === 'number') return value === 1;
|
|
29
|
+
if (typeof value !== 'string') return false;
|
|
30
|
+
return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── API URLs ────────────────────────────────────────────────────────────────
|
|
34
|
+
const QWEN_BASE_URL = process.env.QWEN_BASE_URL || 'https://chat.qwen.ai';
|
|
35
|
+
|
|
36
|
+
export const CHAT_API_URL = process.env.CHAT_API_URL || `${QWEN_BASE_URL}/api/v2/chat/completions`;
|
|
37
|
+
export const CREATE_CHAT_URL = process.env.CREATE_CHAT_URL || `${QWEN_BASE_URL}/api/v2/chats/new`;
|
|
38
|
+
export const CHAT_PAGE_URL = process.env.CHAT_PAGE_URL || `${QWEN_BASE_URL}/`;
|
|
39
|
+
export const TASK_STATUS_URL = process.env.TASK_STATUS_URL || `${QWEN_BASE_URL}/api/v1/tasks/status`;
|
|
40
|
+
export const STS_TOKEN_API_URL = process.env.STS_TOKEN_API_URL || `${QWEN_BASE_URL}/api/v1/files/getstsToken`;
|
|
41
|
+
export const AUTH_SIGNIN_URL = process.env.AUTH_SIGNIN_URL || `${QWEN_BASE_URL}/auth?action=signin`;
|
|
42
|
+
export const OSS_SDK_URL = process.env.OSS_SDK_URL || 'https://gosspublic.alicdn.com/aliyun-oss-sdk-6.20.0.min.js';
|
|
43
|
+
export const MODELS_API_URL = process.env.MODELS_API_URL || `${QWEN_BASE_URL}/api/v2/models`;
|
|
44
|
+
|
|
45
|
+
// ─── Таймауты (мс) ──────────────────────────────────────────────────────────
|
|
46
|
+
export const PAGE_TIMEOUT = Number(process.env.PAGE_TIMEOUT) || 120_000;
|
|
47
|
+
export const AUTH_TIMEOUT = Number(process.env.AUTH_TIMEOUT) || 120_000;
|
|
48
|
+
export const NAVIGATION_TIMEOUT = Number(process.env.NAVIGATION_TIMEOUT) || 60_000;
|
|
49
|
+
export const RETRY_DELAY = Number(process.env.RETRY_DELAY) || 2_000;
|
|
50
|
+
export const STREAMING_CHUNK_DELAY = Number(process.env.STREAMING_CHUNK_DELAY) || 20;
|
|
51
|
+
|
|
52
|
+
// ─── Лимиты ─────────────────────────────────────────────────────────────────
|
|
53
|
+
export const PAGE_POOL_SIZE = Number(process.env.PAGE_POOL_SIZE) || 3;
|
|
54
|
+
export const MAX_FILE_SIZE = Number(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024; // 10 MB
|
|
55
|
+
export const MAX_HISTORY_LENGTH = Number(process.env.MAX_HISTORY_LENGTH) || 100;
|
|
56
|
+
export const MAX_RETRY_COUNT = Number(process.env.MAX_RETRY_COUNT) || 3;
|
|
57
|
+
export const TASK_POLL_MAX_ATTEMPTS = Number(process.env.TASK_POLL_MAX_ATTEMPTS) || 90;
|
|
58
|
+
export const TASK_POLL_INTERVAL = Number(process.env.TASK_POLL_INTERVAL) || 2_000;
|
|
59
|
+
|
|
60
|
+
// ─── Paths (relative to working directory) ───────────────────────────────────────
|
|
61
|
+
export const SESSION_DIR = process.env.SESSION_DIR || 'session';
|
|
62
|
+
export const ACCOUNTS_DIR = 'accounts';
|
|
63
|
+
export const UPLOADS_DIR = process.env.UPLOADS_DIR || 'uploads';
|
|
64
|
+
export const LOGS_DIR = process.env.LOGS_DIR || 'logs';
|
|
65
|
+
export const TEMP_DIR = process.env.TEMP_DIR || 'temp';
|
|
66
|
+
|
|
67
|
+
// Export base directory for use in other modules
|
|
68
|
+
export { BASE_DIR };
|
|
69
|
+
|
|
70
|
+
// ─── Браузер ─────────────────────────────────────────────────────────────────
|
|
71
|
+
export const VIEWPORT_WIDTH = Number(process.env.VIEWPORT_WIDTH) || 1920;
|
|
72
|
+
export const VIEWPORT_HEIGHT = Number(process.env.VIEWPORT_HEIGHT) || 1080;
|
|
73
|
+
export const USER_AGENT = process.env.USER_AGENT || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
|
74
|
+
|
|
75
|
+
// ─── Сервер ──────────────────────────────────────────────────────────────────
|
|
76
|
+
export const PORT = Number(process.env.PORT) || 3264;
|
|
77
|
+
export const HOST = process.env.HOST || '0.0.0.0';
|
|
78
|
+
// DEFAULT_MODEL будет установлен динамически из списка моделей, если не задан в .env
|
|
79
|
+
export const DEFAULT_MODEL = process.env.DEFAULT_MODEL || null;
|
|
80
|
+
export const ALLOW_UNSCOPED_SESSION_CHAT_RESTORE = toBoolean(process.env.ALLOW_UNSCOPED_SESSION_CHAT_RESTORE);
|
|
81
|
+
|
|
82
|
+
// ─── Режим сессий чата ──────────────────────────────────────────────────────
|
|
83
|
+
// FORCE_NEW_CHAT_PER_REQUEST=true: каждый запрос создает новый диалог (как OpenAI API)
|
|
84
|
+
// FORCE_NEW_CHAT_PER_REQUEST=false (по умолчанию): восстанавливает предыдущий диалог
|
|
85
|
+
export const FORCE_NEW_CHAT_PER_REQUEST = toBoolean(process.env.FORCE_NEW_CHAT_PER_REQUEST);
|
|
86
|
+
|
|
87
|
+
// ─── Логирование ─────────────────────────────────────────────────────────────
|
|
88
|
+
export const LOG_LEVEL = process.env.LOG_LEVEL || 'info';
|
|
89
|
+
export const LOG_MAX_SIZE = Number(process.env.LOG_MAX_SIZE) || 5_242_880; // 5 MB
|
|
90
|
+
export const LOG_MAX_FILES = Number(process.env.LOG_MAX_FILES) || 5;
|
|
91
|
+
|
|
92
|
+
// ─── Telegram уведомления ───────────────────────────────────────────────────
|
|
93
|
+
export const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || null;
|
|
94
|
+
export const TELEGRAM_USER_IDS = process.env.TELEGRAM_USER_IDS
|
|
95
|
+
? process.env.TELEGRAM_USER_IDS.split(',').map(id => id.trim()).filter(id => id)
|
|
96
|
+
: [];
|
|
97
|
+
export const TOKEN_EXPIRY_WARNING_MS = Number(process.env.TOKEN_EXPIRY_WARNING_MS) || 3600000; // 1 hour
|
|
98
|
+
|
|
99
|
+
// ─── Telegram прокси ────────────────────────────────────────────────────────
|
|
100
|
+
export const TELEGRAM_PROXY = process.env.TELEGRAM_PROXY || null;
|
|
101
|
+
export const TELEGRAM_PROXY_URL = process.env.TELEGRAM_PROXY_URL || null;
|
|
102
|
+
|
|
103
|
+
// ─── Qwen LLM прокси ────────────────────────────────────────────────────────
|
|
104
|
+
export const QWEN_PROXY = process.env.QWEN_PROXY || null;
|
|
105
|
+
|
|
106
|
+
// ─── Прокси для скачивания файлов ───────────────────────────────────────────
|
|
107
|
+
export const FILE_DOWNLOAD_PROXY = process.env.FILE_DOWNLOAD_PROXY || null;
|
|
108
|
+
|
|
109
|
+
// ─── Генерация изображений ──────────────────────────────────────────────────
|
|
110
|
+
// Режим генерации: 'dashscope' (по умолчанию) или 'browser'
|
|
111
|
+
// 'dashscope' - использует DASHSCOPE_API_KEY напрямую через DashScope API
|
|
112
|
+
// 'browser' - использует браузер (аналогично генерации текста через Qwen Chat)
|
|
113
|
+
export const IMAGE_GENERATION_MODE = process.env.IMAGE_GENERATION_MODE || 'dashscope';
|
|
114
|
+
|
|
115
|
+
// DashScope API ключ (требуется при IMAGE_GENERATION_MODE='dashscope')
|
|
116
|
+
export const DASHSCOPE_API_KEY = process.env.DASHSCOPE_API_KEY || null;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
import morgan from 'morgan';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { LOG_LEVEL, LOG_MAX_SIZE, LOG_MAX_FILES, LOGS_DIR } from '../config.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
const LOG_DIR = path.resolve(__dirname, '..', '..', LOGS_DIR);
|
|
11
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
12
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { combine, timestamp, printf, colorize } = winston.format;
|
|
16
|
+
|
|
17
|
+
const consoleFormat = combine(
|
|
18
|
+
colorize({ all: true }),
|
|
19
|
+
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
20
|
+
printf(({ level, message, timestamp }) => `${timestamp} [${level}]: ${message}`)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const fileFormat = combine(
|
|
24
|
+
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
25
|
+
printf(({ level, message, timestamp }) => `${timestamp} [${level}]: ${message}`)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const customLevels = {
|
|
29
|
+
levels: { error: 0, warn: 1, info: 2, http: 3, debug: 4, raw: 5 },
|
|
30
|
+
colors: { error: 'red', warn: 'yellow', info: 'green', http: 'cyan', debug: 'blue', raw: 'magenta' }
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const logger = winston.createLogger({
|
|
34
|
+
levels: customLevels.levels,
|
|
35
|
+
level: LOG_LEVEL,
|
|
36
|
+
format: fileFormat,
|
|
37
|
+
transports: [
|
|
38
|
+
new winston.transports.File({
|
|
39
|
+
filename: path.join(LOG_DIR, 'combined.log'),
|
|
40
|
+
maxsize: LOG_MAX_SIZE,
|
|
41
|
+
maxFiles: LOG_MAX_FILES
|
|
42
|
+
}),
|
|
43
|
+
new winston.transports.File({
|
|
44
|
+
filename: path.join(LOG_DIR, 'http.log'),
|
|
45
|
+
level: 'http',
|
|
46
|
+
maxsize: LOG_MAX_SIZE,
|
|
47
|
+
maxFiles: LOG_MAX_FILES
|
|
48
|
+
}),
|
|
49
|
+
new winston.transports.File({
|
|
50
|
+
filename: path.join(LOG_DIR, 'error.log'),
|
|
51
|
+
level: 'error',
|
|
52
|
+
maxsize: LOG_MAX_SIZE,
|
|
53
|
+
maxFiles: LOG_MAX_FILES
|
|
54
|
+
}),
|
|
55
|
+
new winston.transports.File({
|
|
56
|
+
filename: path.join(LOG_DIR, 'raw-responses.log'),
|
|
57
|
+
level: 'raw',
|
|
58
|
+
maxsize: LOG_MAX_SIZE,
|
|
59
|
+
maxFiles: LOG_MAX_FILES
|
|
60
|
+
}),
|
|
61
|
+
new winston.transports.Console({ format: consoleFormat })
|
|
62
|
+
]
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
winston.addColors(customLevels.colors);
|
|
66
|
+
|
|
67
|
+
const morganStream = {
|
|
68
|
+
write: (message) => logger.http(message.trim())
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const morganFormat = ':remote-addr :method :url :status :res[content-length] - :response-time ms';
|
|
72
|
+
const httpLogger = morgan(morganFormat, { stream: morganStream });
|
|
73
|
+
|
|
74
|
+
export const logHttpRequest = httpLogger;
|
|
75
|
+
export const logInfo = (message) => logger.info(message);
|
|
76
|
+
export const logError = (message, error) => {
|
|
77
|
+
if (error) {
|
|
78
|
+
logger.error(`${message}: ${error.message}`);
|
|
79
|
+
logger.error(error.stack);
|
|
80
|
+
} else {
|
|
81
|
+
logger.error(message);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
export const logWarn = (message) => logger.warn(message);
|
|
85
|
+
export const logDebug = (message) => logger.debug(message);
|
|
86
|
+
export const logRaw = (message) => logger.raw(message);
|
|
87
|
+
export const logHttp = (message) => logger.http(message);
|
|
88
|
+
|
|
89
|
+
export default { logHttpRequest, logInfo, logError, logWarn, logDebug, logRaw, logHttp };
|