From f6a6bb4f8f5430856df3b45860a491d6581cfbe2 Mon Sep 17 00:00:00 2001 From: yhurynovich Date: Sat, 18 Jul 2026 04:54:27 -0400 Subject: [PATCH 1/4] fix for docker --- src/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.js b/src/config.js index 0917383..49453b2 100644 --- a/src/config.js +++ b/src/config.js @@ -82,7 +82,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 || '127.0.0.1'; +export const HOST = process.env.HOST || '0.0.0.0'; 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); From f5bd0de5b46e0b21ffa1ab93162b4ab065e5bda0 Mon Sep 17 00:00:00 2001 From: yhurynovich Date: Sat, 18 Jul 2026 10:31:29 -0400 Subject: [PATCH 2/4] fix: stop swallowing failed chat responses as empty success The generic error fallback in handleApiError() could produce an empty-string `error` field when neither response.error nor response.statusText were set (common with Node's undici fetch on plain non-2xx responses). Every `if (result.error)` check across routes.js treats an empty string as falsy, so real upstream failures (e.g. Qwen's "Model not found") were silently returned to clients as a 200 with empty content and zeroed usage instead of a proper error. - Guarantee handleApiError's fallback error is always a non-empty string, falling back to `HTTP ` when no better message is available - Bump error-body logging from debug to warn so Qwen's raw error response is visible in logs without changing LOG_LEVEL - Add DEBUG_SSE_CHUNKS-gated raw SSE chunk logging for future streaming diagnostics --- src/api/chat.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/api/chat.js b/src/api/chat.js index 7f0fc0c..bacabf4 100644 --- a/src/api/chat.js +++ b/src/api/chat.js @@ -822,6 +822,10 @@ async function executeApiRequestWithNodeStreaming(apiUrl, payload, token, onChun try { const chunk = JSON.parse(jsonStr); + if (process.env.DEBUG_SSE_CHUNKS === 'true') { + logWarn(`[SSE-DEBUG] ${jsonStr.substring(0, 800)}`); + } + if (chunk.code === 'RateLimited' || (chunk.code && chunk.detail)) { streamError = { status: 429, errorBody: JSON.stringify(chunk) }; finished = true; @@ -1084,7 +1088,7 @@ async function handleApiError(response, tokenObj, requestContext) { const { chatId, retryCount, fileAccountId } = requestContext; logRaw(JSON.stringify(response)); logError(`Ошибка при получении ответа: ${response.error || response.statusText || `HTTP ${response.status || 'unknown'}`}`); - if (response.errorBody) logDebug(`Тело ответа с ошибкой: ${response.errorBody}`); + if (response.errorBody) logWarn(`Тело ответа с ошибкой: ${response.errorBody}`); if (response.html && response.html.includes('Verification')) { setAuthenticationStatus(false); @@ -1166,7 +1170,8 @@ async function handleApiError(response, tokenObj, requestContext) { return { error: `Все токены заблокированы по лимиту (${hours}ч)`, chatId }; } - return { error: response.error || response.statusText, details: response.errorBody || 'Нет дополнительных деталей', chatId }; + const fallbackError = response.error || response.statusText || (response.status ? `HTTP ${response.status}` : 'Неизвестная ошибка ответа'); + return { error: fallbackError, details: response.errorBody || 'Нет дополнительных деталей', chatId }; } // ─── Main public API ───────────────────────────────────────────────────────── From 3337ae49af7dca71ae6e888990e72c0c34261c0e Mon Sep 17 00:00:00 2001 From: yhurynovich Date: Tue, 21 Jul 2026 22:21:38 -0400 Subject: [PATCH 3/4] Title: Fix rate-limit cooldown reading wrong field and wrong time unit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem When Qwen returns an HTTP 429 (rate limit), the account gets cooled down via markRateLimitedByToken(). This cooldown was always defaulting to 24 hours, even when Qwen's own response says the wait is much shorter (e.g. 33 minutes). Root cause Two call sites in src/api/chat.js parse the 429 error body to get the wait time: js const rateInfo = JSON.parse(response.errorBody); const parsedHours = Number(rateInfo.num); Qwen's actual error body nests the wait duration under data.num, not top-level num: json { "success": false, "data": { "code": "RateLimited", "template": "You have reached the daily usage limit. Please wait {{num}} minutes before trying again.", "num": 33 } } Since rateInfo.num is always undefined, Number.isFinite() fails and the code silently falls back to the hardcoded 24-hour default — ~40x longer than the 33 minutes Qwen actually requested. On top of the wrong path, the value is also documented by Qwen's own template string as minutes, while the code treats it as hours. Fix Read the wait value from data.num instead of top-level num. Convert minutes to hours (/ 60) before passing to markRateLimitedByToken(), since that function multiplies by 3600 * 1000. Falls back to 24h if the field is missing/non-numeric, preserving prior safe behavior. Applied identically at both call sites (v2 sendMessage flow and the legacy v1 flow). Impact Accounts hitting Qwen's rate limit now cool down for the actual duration Qwen specifies instead of a flat 24h — significant for single-account deployments, where an inflated cooldown means total downtime. Testing node --check src/api/chat.js passes Verified against a captured 429 response body (data.num: 33, template confirms "minutes") Confirmed no other call sites read .num (grep -n "\.num\b" src/api/*.js) Note Only one 429 sample was available to confirm the "minutes" unit via data.template. If a RateLimited response is ever seen with a different template/unit, this assumption should be revisited. --- docs/QWEN_CHAT_MODELS.md | 6 ++++-- src/AvailableModels.txt | 1 + src/api/chat.js | 12 ++++++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/QWEN_CHAT_MODELS.md b/docs/QWEN_CHAT_MODELS.md index b83036e..6f488e1 100644 --- a/docs/QWEN_CHAT_MODELS.md +++ b/docs/QWEN_CHAT_MODELS.md @@ -1,18 +1,19 @@ # Синхронизация моделей Qwen Chat -Сгенерировано: 2026-06-05T13:56:59.888Z +Сгенерировано: 2026-07-22T02:03:42.778Z Источник: prerendered-метаданные моделей с https://chat.qwen.ai/. ## Модели, которые сейчас видны в Qwen Chat - `qwen3.7-plus` — thinking-режим, аудио, видео, документы, зрение, поиск +- `qwen3.8-max-preview` — thinking-режим, видео, документы, зрение, поиск - `qwen3.7-max` — thinking-режим, документы - `qwen3.6-plus` — thinking-режим, аудио, видео, документы, зрение, поиск ## Добавлено последней синхронизацией -- Новых моделей нет. +- `qwen3.8-max-preview` ## Модели эндпоинта, которых нет в текущих landing-метаданных Qwen Chat @@ -45,6 +46,7 @@ ## Итоговый объединённый список моделей эндпоинта - `qwen3.7-plus` +- `qwen3.8-max-preview` - `qwen3.7-max` - `qwen3.6-plus` - `qwen3.5-plus` diff --git a/src/AvailableModels.txt b/src/AvailableModels.txt index 6b8bb19..cfe661d 100644 --- a/src/AvailableModels.txt +++ b/src/AvailableModels.txt @@ -1,4 +1,5 @@ qwen3.7-plus +qwen3.8-max-preview qwen3.7-max qwen3.6-plus qwen3.5-plus diff --git a/src/api/chat.js b/src/api/chat.js index bacabf4..4a62885 100644 --- a/src/api/chat.js +++ b/src/api/chat.js @@ -1135,8 +1135,10 @@ async function handleApiError(response, tokenObj, requestContext) { let hours = 24; try { const rateInfo = JSON.parse(response.errorBody); - const parsedHours = Number(rateInfo.num); - hours = Number.isFinite(parsedHours) && parsedHours > 0 ? parsedHours : 24; + // Qwen returns the wait time nested at data.num, in MINUTES + // (see data.template: "Please wait {{num}} minutes before trying again.") + const parsedMinutes = Number(rateInfo?.data?.num); + hours = Number.isFinite(parsedMinutes) && parsedMinutes > 0 ? parsedMinutes / 60 : 24; } catch { /* errorBody might not be valid JSON */ } markRateLimitedByToken(tokenObj?.token, hours); @@ -1588,8 +1590,10 @@ export async function createChatV2(model = DEFAULT_MODEL, title = 'Новый ч if (isRateLimited) { let hours = 24; try { - const parsedHours = Number(JSON.parse(structuredErrorBody).num); - hours = Number.isFinite(parsedHours) && parsedHours > 0 ? parsedHours : 24; + // Qwen returns the wait time nested at data.num, in MINUTES + // (see data.template: "Please wait {{num}} minutes before trying again.") + const parsedMinutes = Number(JSON.parse(structuredErrorBody)?.data?.num); + hours = Number.isFinite(parsedMinutes) && parsedMinutes > 0 ? parsedMinutes / 60 : 24; } catch { /* non-JSON body */ } markRateLimitedByToken(tokenObj.token, hours); if (isBrowserAccountId(tokenObj.id) || browserAuthToken === tokenObj.token) { From a94e34cb9fc2c096b4c9215f6926741641c3ea6c Mon Sep 17 00:00:00 2001 From: yhurynovich Date: Wed, 22 Jul 2026 09:55:28 -0400 Subject: [PATCH 4/4] Treat "/new" message as new session Behavior: - When a user sends {"message": "/new"} to /api/chat or /api/chat/completions, the browser session is restarted - The message is NOT forwarded to the Qwen model - Returns success/failure JSON response --- src/api/routes.js | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/api/routes.js b/src/api/routes.js index 75862b3..cb83957 100644 --- a/src/api/routes.js +++ b/src/api/routes.js @@ -1,7 +1,7 @@ import express from 'express'; import { sendMessage, getAllModels, getApiKeys, createChatV2, pollQwenTaskStatus, extractMediaUrl, pagePool, extractAuthToken, preflightFileRequest, testToken } from './chat.js'; import { sendApiResultError } from './apiErrors.js'; -import { getAuthenticationStatus, getBrowserContext } from '../browser/browser.js'; +import { getAuthenticationStatus, getBrowserContext, restartBrowserInHeadlessMode } from '../browser/browser.js'; import { checkAuthentication } from '../browser/auth.js'; import { logInfo, logError, logDebug } from '../logger/index.js'; import { getMappedModel } from './modelMapping.js'; @@ -1066,6 +1066,24 @@ router.post('/chat', async (req, res) => { return res.status(400).json({ error: 'Сообщение не указано' }); } + // Handle /new command - restart browser session + const normalizedMessage = typeof messageContent === 'string' ? messageContent.trim() : ''; + if (normalizedMessage === '/new') { + logInfo('Получена команда /new - перезапуск сессии браузера'); + try { + await restartBrowserInHeadlessMode(); + return res.json({ + success: true, + message: 'Browser session restarted', + chatId: null, + parentId: null + }); + } catch (error) { + logError('Ошибка при перезапуске браузера', error); + return res.status(500).json({ error: 'Failed to restart browser session' }); + } + } + const filePreflight = preflightFileRequest(messageContent, null, getSessionKey(req)); if (filePreflight.error) return sendApiResultError(res, filePreflight); @@ -1397,6 +1415,32 @@ router.post('/chat/completions', async (req, res) => { return res.status(400).json({ error: 'Сообщения не указаны' }); } + // Handle /new command - restart browser session + const lastUserMessage = messages.filter(msg => msg.role === 'user').pop(); + const lastMessageContent = lastUserMessage?.content; + const normalizedLastMessage = typeof lastMessageContent === 'string' ? lastMessageContent.trim() : ''; + if (normalizedLastMessage === '/new') { + logInfo('Получена команда /new - перезапуск сессии браузера'); + try { + await restartBrowserInHeadlessMode(); + return res.json({ + id: 'chatcmpl-' + Date.now(), + object: 'chat.completion', + created: Math.floor(Date.now() / 1000), + model: model || DEFAULT_MODEL, + choices: [{ + index: 0, + message: { role: 'assistant', content: 'Browser session restarted successfully.' }, + finish_reason: 'stop' + }], + usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } + }); + } catch (error) { + logError('Ошибка при перезапуске браузера', error); + return res.status(500).json({ error: 'Failed to restart browser session' }); + } + } + const isMeta = isOpenWebUiMetaRequest(messages); // Используем переданный chatId ИЛИ восстанавливаем из сессии