Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/QWEN_CHAT_MODELS.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -45,6 +46,7 @@
## Итоговый объединённый список моделей эндпоинта

- `qwen3.7-plus`
- `qwen3.8-max-preview`
- `qwen3.7-max`
- `qwen3.6-plus`
- `qwen3.5-plus`
Expand Down
1 change: 1 addition & 0 deletions src/AvailableModels.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
qwen3.7-plus
qwen3.8-max-preview
qwen3.7-max
qwen3.6-plus
qwen3.5-plus
Expand Down
21 changes: 15 additions & 6 deletions src/api/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1131,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);
Expand Down Expand Up @@ -1166,7 +1172,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 ─────────────────────────────────────────────────────────
Expand Down Expand Up @@ -1583,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) {
Expand Down
46 changes: 45 additions & 1 deletion src/api/routes.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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 ИЛИ восстанавливаем из сессии
Expand Down
2 changes: 1 addition & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down