diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..98ab53d --- /dev/null +++ b/.env.example @@ -0,0 +1,86 @@ +# FreeQwenApi — справочник переменных окружения. +# +# Переменные читаются напрямую из окружения процесса (process.env). +# Автозагрузка файла .env в Node-приложении не настроена — задавайте значения +# одним из способов: +# • локально (bash): export PORT=3264 && npm start +# • локально (PowerShell): $env:PORT=3264; npm start +# • docker-compose: блок environment: в docker-compose.yml +# • docker run: docker run -e PORT=3264 ... +# +# Все переменные опциональны — ниже указаны значения по умолчанию. +# Файл можно скопировать в .env как личную шпаргалку (он в .gitignore). + +# ─── Сервер ────────────────────────────────────────────────────────────────── +PORT=3264 +HOST=0.0.0.0 +DEFAULT_MODEL=qwen3.7-max +# Разрешить восстановление чата из сессии без привязки к аккаунту (1/true/yes/on) +ALLOW_UNSCOPED_SESSION_CHAT_RESTORE=false + +# ─── Запуск / меню аккаунтов ───────────────────────────────────────────────── +# Пропустить интерактивное меню выбора аккаунта при старте (нужно для headless/CI/Docker) +SKIP_ACCOUNT_MENU=false +# Полностью неинтерактивный режим (не запрашивать ввод в консоли) +NON_INTERACTIVE=false + +# ─── Лимиты ────────────────────────────────────────────────────────────────── +# Фолбэк-длительность блокировки токена по rate-limit (часы), когда Qwen не +# прислал точное значение. Реализуется отдельным улучшением (feat/qwen-ratelimit-env). +QWEN_RATELIMIT_HOURS=24 +MAX_RETRY_COUNT=3 +MAX_HISTORY_LENGTH=100 +MAX_FILE_SIZE=10485760 +PAGE_POOL_SIZE=3 +TASK_POLL_MAX_ATTEMPTS=90 +TASK_POLL_INTERVAL=2000 + +# ─── Таймауты (мс) ─────────────────────────────────────────────────────────── +PAGE_TIMEOUT=120000 +AUTH_TIMEOUT=120000 +NAVIGATION_TIMEOUT=60000 +RETRY_DELAY=2000 +STREAMING_CHUNK_DELAY=20 + +# ─── Пути (относительно корня проекта) ─────────────────────────────────────── +SESSION_DIR=session +UPLOADS_DIR=uploads +LOGS_DIR=logs + +# ─── Браузер ───────────────────────────────────────────────────────────────── +# Путь к исполняемому файлу Chrome/Chromium. В Docker задаётся = /usr/bin/chromium. +CHROME_PATH= +VIEWPORT_WIDTH=1920 +VIEWPORT_HEIGHT=1080 +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 + +# ─── Логирование ───────────────────────────────────────────────────────────── +LOG_LEVEL=info +LOG_MAX_SIZE=5242880 +LOG_MAX_FILES=5 + +# ─── Генерация изображений/видео (опционально) ─────────────────────────────── +# Прямой ключ DashScope для image/video API. Не обязателен: есть browser-based +# генерация через Qwen Chat. Оставьте пустым, если не используете DashScope. +DASHSCOPE_API_KEY= + +# ─── Адреса Qwen (продвинутое — обычно менять не нужно) ─────────────────────── +# Остальные URL по умолчанию выводятся из QWEN_BASE_URL. +QWEN_BASE_URL=https://chat.qwen.ai +# CHAT_API_URL= +# CREATE_CHAT_URL= +# CHAT_PAGE_URL= +# TASK_STATUS_URL= +# STS_TOKEN_API_URL= +# AUTH_SIGNIN_URL= +# OSS_SDK_URL=https://gosspublic.alicdn.com/aliyun-oss-sdk-6.20.0.min.js + +# ─── Скрипты обслуживания (опционально) ────────────────────────────────────── +# scripts/sync_models.js — синхронизация списка моделей +# QWEN_CHAT_URL= +# QWEN_MODELS_FILE= +# QWEN_MODELS_DOC= +# scripts/smoke_test.js — дымовой тест API +# QWEN_PROXY_BASE_URL=http://localhost:3264 +# QWEN_PROXY_SMOKE_MODEL= +# QWEN_PROXY_API_KEY= diff --git a/README.md b/README.md index 08a2d1d..156598e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ -# FreeQwenApi — ForgetMeAI fork +# FreeQwenApi -> **Локальный OpenAI-compatible прокси к Qwen Chat** от [t.me/forgetmeai](https://t.me/forgetmeai). +> **Локальный OpenAI-compatible прокси к Qwen Chat** ГРАЦ ЕМУ - [t.me/forgetmeai](https://t.me/forgetmeai). > Текст, модели Qwen 3.7, файлы, Open WebUI, Hermes/LiteLLM, а теперь ещё генерация изображений и видео через Qwen Chat. -![ForgetMeAI](https://img.shields.io/badge/ForgetMeAI-t.me%2Fforgetmeai-blue) ![API](https://img.shields.io/badge/API-OpenAI--compatible-green) ![Qwen](https://img.shields.io/badge/Qwen-Chat-purple) @@ -53,6 +52,19 @@ npm run smoke http://localhost:3264/api ``` +## Конфигурация + +Все настройки задаются переменными окружения. Полный список с дефолтами и комментариями — в [`.env.example`](.env.example): порт, модель по умолчанию, таймауты, лимиты, пути, логирование, путь к Chrome и т.д. + +Переменные читаются из окружения процесса. Задайте нужные удобным способом: + +```bash +export PORT=3264 DEFAULT_MODEL=qwen3.7-max # bash +$env:PORT=3264; npm start # PowerShell +``` + +либо через блок `environment:` в `docker-compose.yml` / флаги `-e` у `docker run`. + ## Авторизация Qwen Chat Добавить аккаунт: @@ -364,8 +376,3 @@ curl http://localhost:3264/api/videos/status - URL сгенерированных медиа могут быть временными. - Для production используйте осторожно: это инструмент для экспериментов, демо и локальных workflow. -## От ForgetMeAI - -Если fork помог — подпишитесь: [t.me/forgetmeai](https://t.me/forgetmeai) - -Там практичные AI-инструменты, локальные агенты, open-source находки и честные тесты без корпоративной лапши. diff --git a/src/api/chat.js b/src/api/chat.js index 4d19743..d35d724 100644 --- a/src/api/chat.js +++ b/src/api/chat.js @@ -12,7 +12,8 @@ import { CHAT_API_URL, CREATE_CHAT_URL, CHAT_PAGE_URL, TASK_STATUS_URL, PAGE_TIMEOUT, RETRY_DELAY, PAGE_POOL_SIZE, DEFAULT_MODEL, MAX_RETRY_COUNT, - TASK_POLL_MAX_ATTEMPTS, TASK_POLL_INTERVAL + TASK_POLL_MAX_ATTEMPTS, TASK_POLL_INTERVAL, + RATE_LIMIT_HOURS } from '../config.js'; const __filename = fileURLToPath(import.meta.url); @@ -729,17 +730,19 @@ async function handleApiError(response, tokenObj, message, model, chatId, parent } const { hasValidTokens } = await import('./tokenManager.js'); if (hasValidTokens() && retryCount < MAX_RETRY_COUNT) { - return sendMessage(message, model, chatId, parentId, files, null, null, null, chatType, size, waitForCompletion, retryCount + 1, onChunk); + // chatId/parentId сбрасываем: при смене аккаунта старый чат + // принадлежит прежнему токену и под новым «не существует». + return sendMessage(message, model, null, null, files, null, null, null, chatType, size, waitForCompletion, retryCount + 1, onChunk); } logError('Не осталось валидных токенов или исчерпаны попытки.'); return { error: 'Все токены недействительны (401). Требуется повторная авторизация.', chatId }; } if (response.status === 429 || (response.errorBody && response.errorBody.includes('RateLimited'))) { - let hours = 24; + let hours = RATE_LIMIT_HOURS; try { const rateInfo = JSON.parse(response.errorBody); - hours = Number(rateInfo.num) || 24; + hours = Number(rateInfo.num) || RATE_LIMIT_HOURS; } catch { /* errorBody might not be valid JSON */ } if (tokenObj?.id === 'browser') { @@ -753,7 +756,9 @@ async function handleApiError(response, tokenObj, message, model, chatId, parent authToken = null; const { hasValidTokens } = await import('./tokenManager.js'); if (hasValidTokens() && retryCount < MAX_RETRY_COUNT) { - return sendMessage(message, model, chatId, parentId, files, null, null, null, chatType, size, waitForCompletion, retryCount + 1, onChunk); + // chatId/parentId сбрасываем: при смене аккаунта старый чат + // принадлежит прежнему токену и под новым «не существует». + return sendMessage(message, model, null, null, files, null, null, null, chatType, size, waitForCompletion, retryCount + 1, onChunk); } return { error: `Все токены заблокированы по лимиту (${hours}ч)`, chatId }; } @@ -766,8 +771,17 @@ async function handleApiError(response, tokenObj, message, model, chatId, parent export async function sendMessage(message, model = DEFAULT_MODEL, chatId = null, parentId = null, files = null, tools = null, toolChoice = null, systemMessage = null, chatType = 't2t', size = null, waitForCompletion = true, retryCount = 0, onChunk = null) { if (!availableModels) availableModels = getAvailableModelsFromFile(); + const browserContext = getBrowserContext(); + if (!browserContext) return { error: 'Браузер не инициализирован', chatId }; + + // Резолвим аккаунт ОДИН раз: одним и тем же токеном создаём чат и + // отправляем сообщение — иначе round-robin разнесёт их по разным + // аккаунтам и Qwen вернёт «chat is not exist». + const tokenObj = await resolveAuthToken(browserContext); + if (!tokenObj) return { error: 'Ошибка авторизации: не удалось получить токен', chatId }; + if (!chatId) { - const newChatResult = await createChatV2(model, 'Новый чат', 0, chatType); + const newChatResult = await createChatV2(model, 'Новый чат', 0, chatType, tokenObj); if (newChatResult.error) return { error: 'Не удалось создать чат: ' + newChatResult.error }; chatId = newChatResult.chatId; logInfo(`Создан новый чат v2 с ID: ${chatId}`); @@ -792,12 +806,6 @@ export async function sendMessage(message, model = DEFAULT_MODEL, chatId = null, logInfo(`Тип генерации: ${chatType} (${typeLabels[chatType] || chatType})${size ? `, размер: ${size}` : ''}`); } - const browserContext = getBrowserContext(); - if (!browserContext) return { error: 'Браузер не инициализирован', chatId }; - - const tokenObj = await resolveAuthToken(browserContext); - if (!tokenObj) return { error: 'Ошибка авторизации: не удалось получить токен', chatId }; - let page = null; try { page = await pagePool.getPage(browserContext); @@ -1005,11 +1013,14 @@ export function getAuthToken() { // ─── createChatV2 ──────────────────────────────────────────────────────────── -export async function createChatV2(model = DEFAULT_MODEL, title = 'Новый чат', retryCount = 0, chatType = 't2t') { +export async function createChatV2(model = DEFAULT_MODEL, title = 'Новый чат', retryCount = 0, chatType = 't2t', tokenObj = null) { const browserContext = getBrowserContext(); if (!browserContext) return { error: 'Браузер не инициализирован' }; - const tokenObj = await getAvailableToken(); + // tokenObj может прийти от sendMessage — тогда создание чата и отправка + // идут под ОДНИМ аккаунтом (иначе round-robin создаст чат на одном + // аккаунте, а сообщение уйдёт под другим → «chat is not exist»). + if (!tokenObj) tokenObj = await getAvailableToken(); if (tokenObj?.token) { authToken = tokenObj.token; logInfo(`Используется аккаунт для создания чата: ${tokenObj.id}`); @@ -1054,7 +1065,7 @@ export async function createChatV2(model = DEFAULT_MODEL, title = 'Новый ч if (isTransient && retryCount < MAX_RETRY_COUNT) { logWarn(`Создание чата: ${result.status}, ретрай ${retryCount + 1}/${MAX_RETRY_COUNT} через ${RETRY_DELAY}мс...`); await delay(RETRY_DELAY); - return createChatV2(model, title, retryCount + 1, chatType); + return createChatV2(model, title, retryCount + 1, chatType, tokenObj); } const cleanError = isTransient diff --git a/src/api/chatHistory.js b/src/api/chatHistory.js index a816af3..59bc9d4 100644 --- a/src/api/chatHistory.js +++ b/src/api/chatHistory.js @@ -33,8 +33,33 @@ export function createChat(chatName) { return chatId; } +/** + * Sanitize chatId to prevent path traversal (CWE-22). + * Rejects any value containing path separators, traversal sequences, + * or characters outside the allowed set [a-zA-Z0-9_-]. + * Returns null for invalid values — callers must handle null gracefully. + */ +function sanitizeChatId(chatId) { + if (typeof chatId !== 'string' || !chatId) return null; + // Reject if it contains path separators or traversal sequences + if (chatId.includes('/') || chatId.includes('\\') || chatId.includes('..')) return null; + // Whitelist: only allow alphanumeric, hyphens, and underscores + if (!/^[a-zA-Z0-9_-]+$/.test(chatId)) return null; + return chatId; +} + function getHistoryFilePath(chatId) { - return path.join(HISTORY_DIR, `${chatId}.json`); + const safeChatId = sanitizeChatId(chatId); + if (!safeChatId) { + throw new Error(`Invalid chatId: ${String(chatId).substring(0, 50)}`); + } + const filePath = path.join(HISTORY_DIR, `${safeChatId}.json`); + // Defense-in-depth: verify the resolved path is still inside HISTORY_DIR + const resolved = path.resolve(filePath); + if (!resolved.startsWith(path.resolve(HISTORY_DIR) + path.sep)) { + throw new Error(`Path traversal blocked for chatId: ${String(chatId).substring(0, 50)}`); + } + return filePath; } export function saveHistory(chatId, data) { @@ -120,10 +145,15 @@ export function loadHistory(chatId) { } export function chatExists(chatId) { - const historyFilePath = getHistoryFilePath(chatId); - const exists = fs.existsSync(historyFilePath); - logDebug(`Проверка существования чата ${chatId}: ${exists ? 'найден' : 'не найден'}`); - return exists; + try { + const historyFilePath = getHistoryFilePath(chatId); + const exists = fs.existsSync(historyFilePath); + logDebug(`Проверка существования чата ${chatId}: ${exists ? 'найден' : 'не найден'}`); + return exists; + } catch (error) { + logError(`Invalid chatId in chatExists: ${error.message}`); + return false; + } } export function renameChat(chatId, newName) { @@ -341,4 +371,4 @@ export function deleteChatsAutomatically(criteria = {}) { error: error.message }; } -} \ No newline at end of file +} diff --git a/src/api/routes.js b/src/api/routes.js index 4d01858..d3a39ba 100644 --- a/src/api/routes.js +++ b/src/api/routes.js @@ -593,7 +593,7 @@ function buildOpenAIToolResponse(result, mappedModel, toolCalls) { id: result.id || 'chatcmpl-' + Date.now(), object: 'chat.completion', created: Math.floor(Date.now() / 1000), - model: result.model || mappedModel || 'qwen-max-latest', + model: result.model || mappedModel || DEFAULT_MODEL, choices: [{ index: 0, message: { @@ -616,7 +616,7 @@ function writeToolCallsSse(res, mappedModel, result, toolCalls) { id: result.id || 'chatcmpl-stream', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: result.model || mappedModel || 'qwen-max-latest' + model: result.model || mappedModel || DEFAULT_MODEL }; res.write('data: ' + JSON.stringify({ ...base, @@ -757,7 +757,7 @@ router.post('/chat', async (req, res) => { logInfo(`История содержит ${allMessages.length} сообщений`); } - let mappedModel = model || "qwen-max-latest"; + let mappedModel = model || DEFAULT_MODEL; if (model) { mappedModel = getMappedModel(model); if (mappedModel !== model) { @@ -789,7 +789,7 @@ router.post('/chat', async (req, res) => { id: 'chatcmpl-' + Date.now(), object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: { content: chunk }, finish_reason: null } ] @@ -818,7 +818,7 @@ router.post('/chat', async (req, res) => { id: 'chatcmpl-' + Date.now(), object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: { content: `Ошибка: ${result.error}` }, finish_reason: 'stop' } ] @@ -834,7 +834,7 @@ router.post('/chat', async (req, res) => { id: 'chatcmpl-stream', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: { content }, finish_reason: null } ] @@ -850,7 +850,7 @@ router.post('/chat', async (req, res) => { id: 'chatcmpl-' + Date.now(), object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: {}, finish_reason: 'stop' } ] @@ -864,7 +864,7 @@ router.post('/chat', async (req, res) => { id: 'chatcmpl-stream', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: { content: 'Internal server error' }, finish_reason: 'stop' } ] @@ -1117,7 +1117,7 @@ router.post('/chat/completions', async (req, res) => { logDebug('OpenWebUI meta-запрос: используем отдельный чат (без привязки к сессии)'); } - let mappedModel = model ? getMappedModel(model) : "qwen-max-latest"; + let mappedModel = model ? getMappedModel(model) : DEFAULT_MODEL; if (model && mappedModel !== model) { logInfo(`Модель "${model}" заменена на "${mappedModel}"`); } @@ -1164,7 +1164,7 @@ router.post('/chat/completions', async (req, res) => { id: 'chatcmpl-stream', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: { content: chunk }, finish_reason: null } ] @@ -1213,7 +1213,7 @@ router.post('/chat/completions', async (req, res) => { id: 'chatcmpl-stream', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: { content: `Ошибка: ${result.error}` }, finish_reason: null } ] @@ -1229,7 +1229,7 @@ router.post('/chat/completions', async (req, res) => { id: 'chatcmpl-stream', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: { content }, finish_reason: null } ] @@ -1244,7 +1244,7 @@ router.post('/chat/completions', async (req, res) => { id: 'chatcmpl-stream', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: {}, finish_reason: 'stop' } ] @@ -1258,7 +1258,7 @@ router.post('/chat/completions', async (req, res) => { id: 'chatcmpl-stream', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: { content: 'Internal server error' }, finish_reason: 'stop' } ] @@ -1296,7 +1296,7 @@ router.post('/chat/completions', async (req, res) => { id: result.id || "chatcmpl-" + Date.now(), object: "chat.completion", created: Math.floor(Date.now() / 1000), - model: result.model || mappedModel || "qwen-max-latest", + model: result.model || mappedModel || DEFAULT_MODEL, choices: result.choices || [{ index: 0, message: { @@ -1442,7 +1442,7 @@ router.post('/v1/chat/completions', async (req, res) => { logDebug('OpenWebUI meta-запрос: используем отдельный чат (без привязки к сессии)'); } - let mappedModel = model ? getMappedModel(model) : "qwen-max-latest"; + let mappedModel = model ? getMappedModel(model) : DEFAULT_MODEL; if (model && mappedModel !== model) { logInfo(`Модель "${model}" заменена на "${mappedModel}"`); } @@ -1492,7 +1492,7 @@ router.post('/v1/chat/completions', async (req, res) => { id: 'chatcmpl-' + Date.now(), object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: { content: chunk }, finish_reason: null } ] @@ -1536,7 +1536,7 @@ router.post('/v1/chat/completions', async (req, res) => { id: 'chatcmpl-stream', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: { content: `Ошибка: ${result.error}` }, finish_reason: 'stop' } ] @@ -1552,7 +1552,7 @@ router.post('/v1/chat/completions', async (req, res) => { id: 'chatcmpl-stream', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: { content }, finish_reason: null } ] @@ -1567,7 +1567,7 @@ router.post('/v1/chat/completions', async (req, res) => { id: 'chatcmpl-stream', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: {}, finish_reason: 'stop' } ] @@ -1581,7 +1581,7 @@ router.post('/v1/chat/completions', async (req, res) => { id: 'chatcmpl-stream', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), - model: mappedModel || 'qwen-max-latest', + model: mappedModel || DEFAULT_MODEL, choices: [ { index: 0, delta: { content: 'Internal server error' }, finish_reason: 'stop' } ] @@ -1629,7 +1629,7 @@ router.post('/v1/chat/completions', async (req, res) => { id: result.id || "chatcmpl-" + Date.now(), object: "chat.completion", created: Math.floor(Date.now() / 1000), - model: result.model || mappedModel || "qwen-max-latest", + model: result.model || mappedModel || DEFAULT_MODEL, choices: [{ index: 0, message: { diff --git a/src/api/tokenManager.js b/src/api/tokenManager.js index c82fbea..e1e9591 100644 --- a/src/api/tokenManager.js +++ b/src/api/tokenManager.js @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { logError } from '../logger/index.js'; -import { SESSION_DIR, ACCOUNTS_DIR } from '../config.js'; +import { SESSION_DIR, ACCOUNTS_DIR, RATE_LIMIT_HOURS } from '../config.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -53,7 +53,7 @@ export function hasValidTokens() { return tokens.some(t => (!t.resetAt || new Date(t.resetAt).getTime() <= now) && !t.invalid); } -export function markRateLimited(id, hours = 24) { +export function markRateLimited(id, hours = RATE_LIMIT_HOURS) { const tokens = loadTokens(); const idx = tokens.findIndex(t => t.id === id); if (idx !== -1) { diff --git a/src/config.js b/src/config.js index 7b8e11a..07bad57 100644 --- a/src/config.js +++ b/src/config.js @@ -33,6 +33,8 @@ export const MAX_HISTORY_LENGTH = Number(process.env.MAX_HISTORY_LENGTH) || 100; export const MAX_RETRY_COUNT = Number(process.env.MAX_RETRY_COUNT) || 3; export const TASK_POLL_MAX_ATTEMPTS = Number(process.env.TASK_POLL_MAX_ATTEMPTS) || 90; export const TASK_POLL_INTERVAL = Number(process.env.TASK_POLL_INTERVAL) || 2_000; +// Фолбэк-длительность блокировки токена по rate-limit (часы), когда Qwen не прислал точное значение в ответе. +export const RATE_LIMIT_HOURS = Number(process.env.QWEN_RATELIMIT_HOURS) || 24; // ─── Пути (относительно корня проекта) ─────────────────────────────────────── export const SESSION_DIR = process.env.SESSION_DIR || 'session'; @@ -48,7 +50,7 @@ export const USER_AGENT = process.env.USER_AGENT || 'Mozilla/5.0 (Windows NT 10. // ─── Сервер ────────────────────────────────────────────────────────────────── export const PORT = Number(process.env.PORT) || 3264; export const HOST = process.env.HOST || '0.0.0.0'; -export const DEFAULT_MODEL = process.env.DEFAULT_MODEL || 'qwen-max-latest'; +export const DEFAULT_MODEL = process.env.DEFAULT_MODEL || 'qwen3.7-max'; export const ALLOW_UNSCOPED_SESSION_CHAT_RESTORE = toBoolean(process.env.ALLOW_UNSCOPED_SESSION_CHAT_RESTORE); // ─── Логирование ─────────────────────────────────────────────────────────────