diff --git a/src/api/server.ts b/src/api/server.ts index f02e7d99..77903397 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -9,6 +9,7 @@ import { cache } from '../cache/memory-cache.js' import { Watchdog } from '../core/watchdog.js' import { app as modelsApp } from './models.js' import { chatCompletions, chatCompletionsStop } from '../routes/chat.js' +import { anthropicMessages } from '../routes/anthropic.js' import { uploadFile } from '../routes/upload.js' import { adminApp } from './admin.js' import { getBaseAccountId, makeAccountLaneId } from '../core/account-lanes.js' @@ -63,10 +64,11 @@ app.use('/v1/*', async (c, next) => { if (!apiKey) { return c.json({ error: 'AUTH_REQUIRED=true but no API_KEY is configured' }, 500) } - const auth = c.req.header('Authorization') - if (!auth?.startsWith('Bearer ')) { + const rawAuth = c.req.header('Authorization') || c.req.header('x-api-key') + if (!rawAuth) { return c.json({ error: 'Missing or invalid Authorization header' }, 401) } + const auth = rawAuth.startsWith('Bearer ') ? rawAuth : `Bearer ${rawAuth}` const { resolveUserFromAuthHeader } = await import('../core/user-manager.js') const identity = resolveUserFromAuthHeader(auth) if (!identity) { @@ -84,6 +86,51 @@ app.post('/v1/chat/completions', bodyLimit({ }), chatCompletions) app.post('/v1/chat/completions/stop', chatCompletionsStop) app.post('/v1/upload', uploadFile) +app.post('/v1/messages', bodyLimit({ + maxSize: 52 * 1024 * 1024, + onError: (c: Context) => c.json({ error: { message: 'Request body too large' } }, 413), +}), anthropicMessages) +app.post('/v1/messages/count_tokens', bodyLimit({ + maxSize: 52 * 1024 * 1024, + onError: (c: Context) => c.json({ error: { message: 'Request body too large' } }, 413), +}), async (c) => { + let body: any + try { + body = await c.req.json() + } catch { + return c.json({ type: 'error', error: { type: 'invalid_request_error', message: 'Invalid JSON body' } }, 400) + } + if (!body || typeof body !== 'object' || Array.isArray(body)) { + return c.json({ type: 'error', error: { type: 'invalid_request_error', message: 'Invalid JSON body' } }, 400) + } + const promptParts: string[] = [] + if (typeof body.system === 'string') { + promptParts.push(body.system) + } else if (Array.isArray(body.system)) { + for (const s of body.system) { + if (s && typeof s === 'object' && typeof s.text === 'string') { + promptParts.push(s.text) + } + } + } + if (Array.isArray(body.messages)) { + for (const m of body.messages) { + if (!m || typeof m !== 'object') continue + if (typeof m.content === 'string') { + promptParts.push(m.content) + } else if (Array.isArray(m.content)) { + for (const b of m.content) { + if (b && typeof b === 'object' && typeof b.text === 'string') { + promptParts.push(b.text) + } + } + } + } + } + const { countTokens } = await import('../core/tokenizer.js') + const fullText = promptParts.join('\n') + return c.json({ input_tokens: Math.max(1, countTokens(fullText)) }) +}) // Admin dashboard (served at /admin). app.route('/admin', adminApp) diff --git a/src/routes/anthropic.ts b/src/routes/anthropic.ts new file mode 100644 index 00000000..6ec4724e --- /dev/null +++ b/src/routes/anthropic.ts @@ -0,0 +1,518 @@ +import type { Context } from "hono"; +import { stream as honoStream } from "hono/streaming"; +import crypto from "crypto"; +import type { OpenAIRequest } from "../utils/types.js"; +import { createQwenStream } from "../services/qwen.js"; +import { getNextAccount, markAccountRateLimited, releaseAccountInUse } from "../core/account-manager.js"; +import { loadAccounts } from "../core/accounts.js"; +import { registerStream, removeStream, abortStream } from "../core/stream-registry.js"; +import { checkUserRateLimit, tryAcquireUserSlot, releaseUserSlot } from "../core/user-manager.js"; +import type { UserIdentity } from "../core/user-manager.js"; +import { countTokens } from "../core/tokenizer.js"; +import { QwenStreamParser } from "../utils/qwen-stream-parser.js"; +import { collectNonStreamingResult } from "./stream-handler.js"; +import { trackUsage, trackModelUsage } from "../core/usage-tracker.js"; + +function resolveModelName(model?: string): string { + if (!model) return "qwen3.7-plus"; + const m = model.toLowerCase(); + if (m === "qwen-plus" || m.startsWith("qwen-plus")) return "qwen3.7-plus"; + if (m === "qwen-max" || m.startsWith("qwen-max")) return "qwen3.8-max"; + if (m.includes("max") || m.includes("opus")) { + return m.includes("thinking") ? "qwen3.8-max-thinking" : "qwen3.8-max"; + } + if (m.includes("thinking")) { + return "qwen3.7-plus-thinking"; + } + if (m.startsWith("claude") || m === "sonnet" || m === "haiku") { + return "qwen3.7-plus"; + } + return model; +} + +export async function anthropicMessages(c: Context) { + const user = (c as any).get?.("user") as UserIdentity | undefined; + let userSlotHeld = false; + let userSlotReleased = false; + const completionId = `comp_${crypto.randomUUID().replace(/-/g, "")}`; + + const releaseUserSlotOnce = () => { + if (!userSlotHeld || userSlotReleased || !user) return; + releaseUserSlot(user.id); + userSlotReleased = true; + }; + + let body: any; + try { + body = await c.req.json(); + if (!body || typeof body !== "object" || Array.isArray(body)) { + return c.json({ type: "error", error: { type: "invalid_request_error", message: "Invalid JSON body" } }, 400); + } + } catch { + return c.json({ type: "error", error: { type: "invalid_request_error", message: "Invalid JSON body" } }, 400); + } + + try { + const isStream = Boolean(body.stream); + const rawModel = body.model || "qwen3.7-plus"; + const targetModel = resolveModelName(rawModel); + const isThinkingModel = targetModel.includes("thinking"); + + if (user) { + if (!checkUserRateLimit(user.id, user.rateLimitRpm)) { + return c.json({ + type: "error", + error: { + type: "rate_limit_error", + message: `Rate limit exceeded for user ${user.id}`, + }, + }, 429); + } + + if (!tryAcquireUserSlot(user.id, user.maxConcurrency)) { + return c.json({ + type: "error", + error: { + type: "rate_limit_error", + message: `Concurrency limit exceeded for user ${user.id} (max ${user.maxConcurrency})`, + }, + }, 429); + } + userSlotHeld = true; + } + + const messages = Array.isArray(body.messages) ? body.messages : []; + const openAIMessages: OpenAIRequest["messages"] = []; + + if (body.system) { + if (typeof body.system === "string") { + openAIMessages.push({ role: "system", content: body.system }); + } else if (Array.isArray(body.system)) { + const sysText = body.system.map((s: any) => s?.text || "").join("\n"); + openAIMessages.push({ role: "system", content: sysText }); + } + } + + for (const msg of messages) { + if (!msg || typeof msg !== "object") continue; + const role = msg.role === "assistant" ? "assistant" : "user"; + if (typeof msg.content === "string") { + openAIMessages.push({ role, content: msg.content }); + } else if (Array.isArray(msg.content)) { + let textParts = ""; + for (const block of msg.content) { + if (!block || typeof block !== "object") continue; + if (block.type === "text") { + textParts += (block.text || "") + "\n"; + } else if (block.type === "tool_result") { + const contentStr = typeof block.content === "string" ? block.content : JSON.stringify(block.content || ""); + textParts += `[Tool Result for ${block.tool_use_id}]: ${contentStr}\n`; + } else if (block.type === "tool_use") { + textParts += `[Tool Use: ${block.name} (${block.id})]: ${JSON.stringify(block.input || {})}\n`; + } + } + openAIMessages.push({ role, content: textParts.trim() }); + } + } + + const rawPromptText = openAIMessages.map(m => `${m.role}: ${m.content}`).join("\n"); + const inputTokens = Math.max(1, countTokens(rawPromptText)); + + const finalPrompt = openAIMessages.map(m => { + const role = m.role === "assistant" ? "Assistant" : (m.role === "system" ? "System" : "User"); + return `${role}: ${m.content}`; + }).join("\n\n") + "\n\nAssistant:"; + + const baseStreamOptions = { + forceBootstrap: false, + }; + + const stopToken = crypto.randomUUID(); + const hasTools = Array.isArray(body.tools) && body.tools.length > 0; + + let streamResult: { stream: ReadableStream; uiSessionId: string }; + const accounts = loadAccounts(); + const isGuestModeOnly = accounts.length === 0; + + if (isGuestModeOnly) { + const result = await createQwenStream( + finalPrompt, + isThinkingModel, + targetModel, + null, + "guest", + undefined, + undefined, + { ...baseStreamOptions, forceBootstrap: true } + ); + registerStream(completionId, { + abortController: result.controller, + accountId: "guest", + uiSessionId: result.uiSessionId, + targetResponseId: "", + headers: result.headers, + stopToken, + }); + streamResult = { stream: result.stream, uiSessionId: result.uiSessionId }; + } else { + const selectedAccount = getNextAccount(); + if (!selectedAccount) { + const result = await createQwenStream( + finalPrompt, + isThinkingModel, + targetModel, + null, + "guest", + undefined, + undefined, + { ...baseStreamOptions, forceBootstrap: true } + ); + registerStream(completionId, { + abortController: result.controller, + accountId: "guest", + uiSessionId: result.uiSessionId, + targetResponseId: "", + headers: result.headers, + stopToken, + }); + streamResult = { stream: result.stream, uiSessionId: result.uiSessionId }; + } else { + const accountId = selectedAccount.id; + try { + const result = await createQwenStream( + finalPrompt, + isThinkingModel, + targetModel, + null, + accountId, + undefined, + undefined, + baseStreamOptions + ); + registerStream(completionId, { + abortController: result.controller, + accountId: result.accountId, + uiSessionId: result.uiSessionId, + targetResponseId: "", + headers: result.headers, + stopToken, + }); + releaseAccountInUse(accountId); + streamResult = { stream: result.stream, uiSessionId: result.uiSessionId }; + } catch (err: any) { + releaseAccountInUse(accountId); + console.warn(`[Anthropic] Account ${accountId} stream failed, falling back to guest: ${err?.message}`); + if (/rate limit|429/i.test(err?.message || "")) { + markAccountRateLimited(accountId); + } + const result = await createQwenStream( + finalPrompt, + isThinkingModel, + targetModel, + null, + "guest", + undefined, + undefined, + { ...baseStreamOptions, forceBootstrap: true } + ); + registerStream(completionId, { + abortController: result.controller, + accountId: "guest", + uiSessionId: result.uiSessionId, + targetResponseId: "", + headers: result.headers, + stopToken, + }); + streamResult = { stream: result.stream, uiSessionId: result.uiSessionId }; + } + } + } + + const onComplete = (outputTokens = 1) => { + removeStream(completionId); + releaseUserSlotOnce(); + if (user?.id) { + trackUsage(user.id, rawPromptText, false); + } + trackModelUsage(targetModel); + }; + + if (isStream) { + return handleAnthropicStream( + c, + streamResult.stream, + rawModel, + completionId, + streamResult.uiSessionId, + inputTokens, + hasTools, + body.tools || [], + onComplete + ); + } else { + return await handleAnthropicNonStreaming( + c, + streamResult.stream, + rawModel, + completionId, + streamResult.uiSessionId, + inputTokens, + hasTools, + body.tools || [], + onComplete + ); + } + + } catch (err: any) { + if (completionId) removeStream(completionId); + releaseUserSlotOnce(); + console.error("[Anthropic API Error]:", err); + return c.json({ type: "error", error: { type: "api_error", message: err.message || "Internal Server Error" } }, 500); + } +} + +function handleAnthropicStream( + c: Context, + stream: ReadableStream, + model: string, + completionId: string, + uiSessionId: string, + inputTokens: number, + hasTools: boolean, + tools: any[], + onComplete?: (outTokens: number) => void, +) { + const socket = (c.env as any)?.incoming?.socket || (c.req.raw as any).socket; + if (socket && typeof socket.setNoDelay === "function") { + socket.setNoDelay(true); + } + + c.header("Content-Type", "text/event-stream"); + c.header("Cache-Control", "no-cache, no-transform"); + c.header("Connection", "keep-alive"); + c.header("X-Accel-Buffering", "no"); + + return honoStream(c, async (streamWriter: any) => { + streamWriter.onAbort?.(() => { + abortStream(completionId); + }); + + let heartbeatInterval: any; + let blockIndex = 0; + let textBlockOpen = false; + let totalOutputTokens = 0; + let stopReason = "end_turn"; + let reader: any; + const msgId = `msg_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`; + + const sendEvent = (event: string, data: any) => { + streamWriter.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); + }; + + try { + sendEvent("message_start", { + type: "message_start", + message: { + id: msgId, + type: "message", + role: "assistant", + model, + content: [], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: inputTokens, output_tokens: 1 }, + }, + }); + + heartbeatInterval = setInterval(async () => { + try { + await streamWriter.write(": keep-alive\n\n"); + } catch { + clearInterval(heartbeatInterval); + } + }, 15000); + + reader = stream.getReader(); + const decoder = new TextDecoder(); + let streamEnded = false; + let rawBuffer = ""; + + const formattedTools: any[] = tools.map((t: any) => ({ + type: "function", + function: { + name: t.name, + description: t.description || "", + parameters: t.input_schema || {}, + }, + })); + + const qwenParser = new QwenStreamParser(uiSessionId, { + tools: hasTools ? formattedTools : [], + onAnswer: (deltaText: string) => { + if (!deltaText) return; + if (!textBlockOpen) { + textBlockOpen = true; + sendEvent("content_block_start", { + type: "content_block_start", + index: blockIndex, + content_block: { type: "text", text: "" }, + }); + } + sendEvent("content_block_delta", { + type: "content_block_delta", + index: blockIndex, + delta: { type: "text_delta", text: deltaText }, + }); + totalOutputTokens += Math.ceil(deltaText.length / 4); + }, + onToolCall: (tc) => { + stopReason = "tool_use"; + if (textBlockOpen) { + sendEvent("content_block_stop", { type: "content_block_stop", index: blockIndex }); + textBlockOpen = false; + blockIndex++; + } + const toolId = tc.id || `toolu_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`; + const argsStr = typeof tc.arguments === "string" ? tc.arguments : JSON.stringify(tc.arguments || {}); + + sendEvent("content_block_start", { + type: "content_block_start", + index: blockIndex, + content_block: { + type: "tool_use", + id: toolId, + name: tc.name, + input: {}, + }, + }); + sendEvent("content_block_delta", { + type: "content_block_delta", + index: blockIndex, + delta: { + type: "input_json_delta", + partial_json: argsStr, + }, + }); + sendEvent("content_block_stop", { type: "content_block_stop", index: blockIndex }); + blockIndex++; + }, + }); + + while (!streamEnded) { + const { done, value } = await reader.read(); + if (done) break; + + rawBuffer += decoder.decode(value, { stream: true }); + const lines = rawBuffer.split("\n"); + rawBuffer = lines.pop() || ""; + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed === "data: [DONE]") { + streamEnded = true; + break; + } + if (trimmed.startsWith("data: ")) { + qwenParser.parseLine(trimmed.slice(6)); + } + } + } + + if (rawBuffer.trim() && rawBuffer.trim().startsWith("data: ") && rawBuffer.trim() !== "data: [DONE]") { + qwenParser.parseLine(rawBuffer.trim().slice(6)); + } + + if (textBlockOpen) { + sendEvent("content_block_stop", { type: "content_block_stop", index: blockIndex }); + } + + const finalOutputTokens = qwenParser.usage?.completionTokens && qwenParser.usage.completionTokens > 0 + ? qwenParser.usage.completionTokens + : Math.max(1, totalOutputTokens); + + sendEvent("message_delta", { + type: "message_delta", + delta: { stop_reason: stopReason, stop_sequence: null }, + usage: { output_tokens: finalOutputTokens }, + }); + sendEvent("message_stop", { type: "message_stop" }); + + } catch (err: any) { + console.error("[Anthropic Stream Error]:", err); + } finally { + if (heartbeatInterval) clearInterval(heartbeatInterval); + if (reader) reader.cancel().catch(() => {}); + onComplete?.(totalOutputTokens || 1); + } + }); +} + +async function handleAnthropicNonStreaming( + c: Context, + stream: ReadableStream, + model: string, + completionId: string, + uiSessionId: string, + inputTokens: number, + hasTools: boolean, + tools: any[], + onComplete?: (outTokens: number) => void, +) { + const formattedTools: any[] = tools.map((t: any) => ({ + type: "function", + function: { + name: t.name, + description: t.description || "", + parameters: t.input_schema || {}, + }, + })); + + const result = await collectNonStreamingResult( + c, + stream, + completionId, + model, + uiSessionId, + hasTools, + formattedTools, + () => {}, + ); + + const contentBlocks: any[] = []; + if (result.content) { + contentBlocks.push({ type: "text", text: result.content }); + } + if (result.toolCalls && Array.isArray(result.toolCalls)) { + for (const tc of result.toolCalls) { + let inputObj = {}; + try { + inputObj = typeof tc.function?.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function?.arguments || {}; + } catch { + inputObj = { raw: tc.function?.arguments }; + } + contentBlocks.push({ + type: "tool_use", + id: tc.id || `toolu_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`, + name: tc.function?.name, + input: inputObj, + }); + } + } + + const outTokens = result.body?.usage?.completion_tokens || Math.ceil((result.content || "").length / 4); + onComplete?.(Math.max(1, outTokens)); + + return c.json({ + id: `msg_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`, + type: "message", + role: "assistant", + model, + content: contentBlocks, + stop_reason: result.toolCalls && result.toolCalls.length > 0 ? "tool_use" : "end_turn", + stop_sequence: null, + usage: { + input_tokens: inputTokens, + output_tokens: Math.max(1, outTokens), + }, + }); +} diff --git a/src/services/browser-manager.ts b/src/services/browser-manager.ts index 4668e4f2..76af818f 100644 --- a/src/services/browser-manager.ts +++ b/src/services/browser-manager.ts @@ -501,10 +501,16 @@ export async function resetBrowserProfile(cacheKey: string, accountId?: string): } markAccountNotReady(accountId || cacheKey); markAccountNotReady(profileId); - fs.rmSync(profilePath, { recursive: true, force: true }); - fs.rmSync(storageStatePath(profileId), { force: true }); - - console.warn(`[Playwright] Cleared browser profile for ${cacheKey}: ${profilePath}`); + const { getAccountCredentials } = await import("../core/accounts.js"); + const hasCreds = accountId ? !!getAccountCredentials(getBaseAccountId(accountId))?.password : false; + const isManualNamedAccount = Boolean(accountId && accountId !== "guest" && !hasCreds); + if (isManualNamedAccount) { + console.warn(`[Playwright] Preserving cookies/storage for manual login account: ${cacheKey}`); + } else { + fs.rmSync(profilePath, { recursive: true, force: true }); + fs.rmSync(storageStatePath(profileId), { force: true }); + console.warn(`[Playwright] Cleared browser profile for ${cacheKey}: ${profilePath}`); + } } catch (err: any) { console.warn(`[Playwright] Failed to clear browser profile for ${cacheKey}: ${err.message}`); } @@ -601,26 +607,34 @@ export async function initPlaywrightForAccount(account: QwenAccount, _headless = await loginToQwenWithContext(acctContext, acctPage, account.email, account.password); } + let navigated = false; try { await acctPage.goto('https://chat.qwen.ai/c/new-chat', { waitUntil: 'domcontentloaded', timeout: config.timeouts.navigation }); + navigated = true; const url = acctPage.url(); if (url.includes('auth') || url.includes('login')) { if (account.email && account.password) { - console.log(`[Playwright] Session expired for ${account.email}, re-logging in...`); + console.log(`[Playwright] Session expired for account ${account.id}, re-logging in...`); await loginToQwenWithContext(acctContext, acctPage, account.email, account.password); await acctPage.goto('https://chat.qwen.ai/c/new-chat', { waitUntil: 'domcontentloaded', timeout: config.timeouts.navigation }); + navigated = true; } else { console.warn(`[Playwright] Session expired for account ${account.id} but no credentials available for re-login.`); } } else { - console.log(`[Playwright] Session validated for ${account.email}.`); + console.log(`[Playwright] Session validated for account ${account.id}.`); } } catch (err: any) { - console.warn(`[Playwright] Failed to validate session for ${account.email}: ${err.message}`); + console.warn(`[Playwright] Failed to validate session for account ${account.id}: ${err.message}`); } - if (await hasValidAuthCookie(acctPage)) { + const finalUrl = acctPage.url(); + const sessionOk = navigated && !finalUrl.includes("auth") && !finalUrl.includes("login") && finalUrl.startsWith("http"); + if (sessionOk && (await hasValidAuthCookie(acctPage))) { await saveStorageState(acctContext, baseAccountId); + const { markAccountReady } = await import("../core/account-manager.js"); + markAccountReady(account.id); + markAccountReady(baseAccountId); } } diff --git a/src/services/header-interceptor.ts b/src/services/header-interceptor.ts index bbf296d9..a29b4afe 100644 --- a/src/services/header-interceptor.ts +++ b/src/services/header-interceptor.ts @@ -78,8 +78,8 @@ export async function getBasicHeaders(accountId?: string): Promise<{ cookie: str let bxUmidtoken = cache.currentHeaders['bx-umidtoken']; const bxV = cache.currentHeaders['bx-v'] || '2.5.36'; - if (!bxUa || !bxUmidtoken) { - console.log(`[Playwright] Missing bx-ua/bx-umidtoken for ${cacheKey}, triggering header interception...`); + if (!cache.cachedQwenHeaders && config.directFetch.enabled && (!bxUa || !bxUmidtoken)) { + console.log(`[Playwright] Capturing initial headers for ${cacheKey}...`); try { const result = await getQwenHeaders(true, accountId); bxUa = result.headers['bx-ua']; @@ -97,9 +97,7 @@ export async function getBasicHeaders(accountId?: string): Promise<{ cookie: str } } - if (bxUa && bxUmidtoken) { - markAccountReady(cacheKey); - } + markAccountReady(cacheKey); return { cookie, userAgent, bxV, bxUa, bxUmidtoken }; } @@ -184,7 +182,7 @@ export async function getGuestHeaders(): Promise> { await humanType(guestPage!, inputSelector, 'Hello'); await sleep(humanDelay(800, 1500)); - const selectors = ['.message-input-right-button-send .send-button', '.chat-prompt-send-button', 'button.send-button']; + const selectors = ['.message-input-right-button-send .send-button', '.chat-prompt-send-button', 'button.send-button', 'button[type="submit"]', 'button[aria-label*="Send"]', 'button[aria-label*="Enviar"]', 'button[data-testid*="send"]']; let clicked = false; for (const selector of selectors) { const btn = await guestPage!.$(selector); @@ -463,8 +461,8 @@ async function _getQwenHeadersInternalOnce(forceNew = false, accountId?: string) 'user-agent': reqHeaders['user-agent'] || '' }; - if (!extractedHeaders.cookie || !extractedHeaders['bx-ua']) { - console.log(`[Playwright] Intercepted request missing critical headers for ${cacheKey}, skipping...`); + if (!extractedHeaders.cookie) { + console.log(`[Playwright] Intercepted request missing cookie for ${cacheKey}, skipping...`); await route.continue(); return; } diff --git a/src/services/stream-creator.ts b/src/services/stream-creator.ts index 1052751e..75c7b37d 100644 --- a/src/services/stream-creator.ts +++ b/src/services/stream-creator.ts @@ -22,8 +22,8 @@ const BASE_TIMEOUT_MS = 120000; const TIMEOUT_PER_MB = 30000; function assertAntiBotHeaders(headers: Record, label: string): void { - if (!headers['cookie'] || !headers['user-agent'] || !headers['bx-ua'] || !headers['bx-umidtoken'] || !headers['bx-v']) { - throw new Error(`${label} missing required browser anti-bot headers`); + if (!headers["cookie"] || !headers["user-agent"]) { + throw new Error(`${label} missing required cookie or user-agent`); } } diff --git a/src/services/warm-pool.ts b/src/services/warm-pool.ts index b88fe0a0..a9cfbf72 100644 --- a/src/services/warm-pool.ts +++ b/src/services/warm-pool.ts @@ -64,15 +64,15 @@ function isWarmChatInFlight(accountId: string, chatId: string) { async function getBasicQwenHeaders(accountId?: string): Promise> { const { cookie, userAgent, bxV, bxUa, bxUmidtoken } = await getBasicHeaders(accountId); - if (!cookie || !userAgent || !bxV || !bxUa || !bxUmidtoken) { - throw new Error('Missing required browser anti-bot headers for warm pool'); + if (!cookie || !userAgent) { + throw new Error("Missing required cookie or user-agent for warm pool"); } return { cookie, - 'user-agent': userAgent, - 'bx-v': bxV, - 'bx-ua': bxUa, - 'bx-umidtoken': bxUmidtoken, + "user-agent": userAgent, + "bx-v": bxV || "2.5.36", + "bx-ua": bxUa || "", + "bx-umidtoken": bxUmidtoken || "", }; }