mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 02:02:13 +03:00
feat: add Qwen Web (chat.qwen.ai) cookie provider (#2947)
Integrated into release/v3.8.8
This commit is contained in:
@@ -3940,6 +3940,26 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
],
|
||||
},
|
||||
|
||||
"qwen-web": {
|
||||
id: "qwen-web",
|
||||
alias: "qw",
|
||||
format: "openai",
|
||||
executor: "qwen-web",
|
||||
baseUrl: "https://chat.qwen.ai/api/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: [
|
||||
{ id: "qwen-plus", name: "Qwen Plus" },
|
||||
{ id: "qwen-max", name: "Qwen Max" },
|
||||
{ id: "qwen-turbo", name: "Qwen Turbo" },
|
||||
{ id: "qwen3-plus", name: "Qwen3 Plus" },
|
||||
{ id: "qwen3-max", name: "Qwen3 Max" },
|
||||
{ id: "qwen3-flash", name: "Qwen3 Flash" },
|
||||
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
|
||||
{ id: "qwen3-coder-flash", name: "Qwen3 Coder Flash" },
|
||||
],
|
||||
},
|
||||
|
||||
codestral: {
|
||||
id: "codestral",
|
||||
alias: "codestral",
|
||||
|
||||
@@ -45,6 +45,7 @@ import { VeniceWebExecutor } from "./venice-web.ts";
|
||||
import { V0VercelWebExecutor } from "./v0-vercel-web.ts";
|
||||
import { KimiWebExecutor } from "./kimi-web.ts";
|
||||
import { DoubaoWebExecutor } from "./doubao-web.ts";
|
||||
import { QwenWebExecutor } from "./qwen-web.ts";
|
||||
|
||||
const executors = {
|
||||
antigravity: new AntigravityExecutor(),
|
||||
@@ -127,6 +128,8 @@ const executors = {
|
||||
kimi: new KimiWebExecutor(), // Alias
|
||||
"doubao-web": new DoubaoWebExecutor(),
|
||||
db: new DoubaoWebExecutor(), // Alias
|
||||
"qwen-web": new QwenWebExecutor(),
|
||||
qw: new QwenWebExecutor(), // Alias
|
||||
};
|
||||
|
||||
const defaultCache = new Map();
|
||||
@@ -182,3 +185,4 @@ export { DeepSeekWebWithAutoRefreshExecutor } from "./deepseek-web-with-auto-ref
|
||||
export { AdaptaWebExecutor } from "./adapta-web.ts";
|
||||
export { T3ChatWebExecutor } from "./t3-chat-web.ts";
|
||||
export { InnerAiExecutor } from "./inner-ai.ts";
|
||||
export { QwenWebExecutor } from "./qwen-web.ts";
|
||||
|
||||
159
open-sse/executors/qwen-web.ts
Normal file
159
open-sse/executors/qwen-web.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* QwenWebExecutor — Alibaba Tongyi Qwen Chat via chat.qwen.ai
|
||||
*
|
||||
* Routes requests through Qwen's consumer chat API.
|
||||
* Chinese market provider with strong vision, coding, and reasoning models.
|
||||
*
|
||||
* Auth: Token from chat.qwen.ai Local Storage or tongyi_sso_ticket cookie
|
||||
* Endpoint: POST https://chat.qwen.ai/api/chat/completions
|
||||
* Format: OpenAI-compatible
|
||||
*/
|
||||
import { BaseExecutor, type ExecuteInput } from "./base.ts";
|
||||
import { makeExecutorErrorResult as makeErrorResult } from "../utils/error.ts";
|
||||
|
||||
const BASE_URL = "https://chat.qwen.ai";
|
||||
const CHAT_URL = `${BASE_URL}/api/chat/completions`;
|
||||
const 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";
|
||||
|
||||
export class QwenWebExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("qwen-web", { id: "qwen-web", baseUrl: BASE_URL });
|
||||
}
|
||||
|
||||
async execute(input: ExecuteInput) {
|
||||
const { body, credentials, signal, stream: wantStream } = input;
|
||||
const bodyObj = (body || {}) as Record<string, unknown>;
|
||||
const rawToken = String(credentials?.apiKey ?? credentials?.accessToken ?? "").trim();
|
||||
|
||||
const messages = (bodyObj.messages as Array<{ role: string; content: string }>) || [];
|
||||
const modelId = (bodyObj.model as string) || "qwen-plus";
|
||||
|
||||
const reqBody = {
|
||||
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
||||
model: modelId,
|
||||
stream: wantStream,
|
||||
max_tokens: (bodyObj.max_tokens as number) || 4096,
|
||||
};
|
||||
|
||||
const reqHeaders: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": USER_AGENT,
|
||||
Accept: wantStream ? "text/event-stream" : "application/json",
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
};
|
||||
if (rawToken) {
|
||||
reqHeaders["Authorization"] = `Bearer ${rawToken}`;
|
||||
}
|
||||
|
||||
let upstream: Response;
|
||||
try {
|
||||
upstream = await fetch(CHAT_URL, {
|
||||
method: "POST",
|
||||
headers: reqHeaders,
|
||||
body: JSON.stringify(reqBody),
|
||||
signal,
|
||||
});
|
||||
} catch (err) {
|
||||
return makeErrorResult(
|
||||
502,
|
||||
`Qwen fetch failed: ${err instanceof Error ? err.message : "unknown"}`,
|
||||
body,
|
||||
CHAT_URL
|
||||
);
|
||||
}
|
||||
|
||||
if (!upstream.ok) {
|
||||
const errText = await upstream.text().catch(() => "");
|
||||
if (upstream.status === 401) {
|
||||
return makeErrorResult(
|
||||
401,
|
||||
"Qwen authentication failed. Your token may have expired. " +
|
||||
"Get a fresh token from chat.qwen.ai (DevTools → Application → Local Storage → token)",
|
||||
body,
|
||||
CHAT_URL
|
||||
);
|
||||
}
|
||||
return makeErrorResult(upstream.status, `Qwen error: ${errText}`, body, CHAT_URL);
|
||||
}
|
||||
|
||||
if (!wantStream) {
|
||||
const data = (await upstream.json()) as Record<string, unknown>;
|
||||
const content =
|
||||
(data?.choices as Array<{ message?: { content?: string } }>)?.[0]?.message?.content ||
|
||||
(data?.content as string) ||
|
||||
"";
|
||||
return {
|
||||
response: new Response(
|
||||
JSON.stringify({
|
||||
id: `chatcmpl-qwen-${Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: modelId,
|
||||
choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }],
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
),
|
||||
url: CHAT_URL,
|
||||
headers: reqHeaders,
|
||||
transformedBody: reqBody,
|
||||
};
|
||||
}
|
||||
|
||||
// Streaming
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const reader = upstream.body?.getReader();
|
||||
if (!reader) { controller.close(); return; }
|
||||
let buffer = "";
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data:")) continue;
|
||||
const data = line.slice(5).trim();
|
||||
if (data === "[DONE]") { controller.enqueue(encoder.encode("data: [DONE]\n\n")); continue; }
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const text = parsed.choices?.[0]?.delta?.content || parsed.choices?.[0]?.text || "";
|
||||
if (text) {
|
||||
const chunk = {
|
||||
id: `chatcmpl-qwen-${Date.now()}`,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: modelId,
|
||||
choices: [{ index: 0, delta: { content: text }, finish_reason: null }],
|
||||
};
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
||||
}
|
||||
} catch {
|
||||
/* skip unparseable chunks */
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (!signal?.aborted) controller.error(err);
|
||||
} finally {
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
response: new Response(stream, {
|
||||
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" },
|
||||
}),
|
||||
url: CHAT_URL,
|
||||
headers: reqHeaders,
|
||||
transformedBody: reqBody,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -141,6 +141,12 @@ export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = {
|
||||
placeholder: "session=... or full Cookie header from doubao.com",
|
||||
acceptsFullCookieHeader: true,
|
||||
},
|
||||
"qwen-web": {
|
||||
kind: "token",
|
||||
credentialName: "token",
|
||||
placeholder: "Paste your Qwen token from chat.qwen.ai (Local Storage → token)",
|
||||
acceptsFullCookieHeader: false,
|
||||
},
|
||||
} satisfies Record<keyof typeof WEB_COOKIE_PROVIDERS, WebSessionCredentialRequirement>;
|
||||
|
||||
export function getWebSessionCredentialRequirement(
|
||||
|
||||
@@ -516,6 +516,20 @@ export const WEB_COOKIE_PROVIDERS = {
|
||||
subscriptionRisk: true,
|
||||
riskNoticeVariant: "webCookie",
|
||||
},
|
||||
"qwen-web": {
|
||||
id: "qwen-web",
|
||||
alias: "qw",
|
||||
name: "Qwen Web (Free)",
|
||||
icon: "auto_awesome",
|
||||
color: "#10B981",
|
||||
textIcon: "QW",
|
||||
website: "https://chat.qwen.ai",
|
||||
hasFree: true,
|
||||
freeNote: "Free — Qwen models via chat.qwen.ai with login token. No subscription required.",
|
||||
authHint:
|
||||
"Open chat.qwen.ai, log in, then open DevTools → Application → Local Storage → " +
|
||||
'copy the "token" value (or use tongyi_sso_ticket cookie as Bearer token).',
|
||||
},
|
||||
};
|
||||
|
||||
// API Key Providers
|
||||
|
||||
@@ -8,6 +8,7 @@ const { VeniceWebExecutor } = await import("../../open-sse/executors/venice-web.
|
||||
const { V0VercelWebExecutor } = await import("../../open-sse/executors/v0-vercel-web.ts");
|
||||
const { KimiWebExecutor } = await import("../../open-sse/executors/kimi-web.ts");
|
||||
const { DoubaoWebExecutor } = await import("../../open-sse/executors/doubao-web.ts");
|
||||
const { QwenWebExecutor } = await import("../../open-sse/executors/qwen-web.ts");
|
||||
const { getExecutor, hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts");
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
@@ -175,6 +176,22 @@ test("Doubao Web sets correct provider", () => {
|
||||
assert.equal(executor.getProvider(), "doubao-web");
|
||||
});
|
||||
|
||||
// ── Registration Tests (Qwen Web) ────────────────────────────────────────────
|
||||
|
||||
test("Qwen Web executor is registered", () => {
|
||||
assert.ok(hasSpecializedExecutor("qwen-web"));
|
||||
assert.ok(hasSpecializedExecutor("qw"));
|
||||
const executor = getExecutor("qwen-web");
|
||||
assert.ok(executor instanceof QwenWebExecutor);
|
||||
});
|
||||
|
||||
// ── Constructor Tests (Qwen Web) ─────────────────────────────────────────────
|
||||
|
||||
test("Qwen Web sets correct provider", () => {
|
||||
const executor = new QwenWebExecutor();
|
||||
assert.equal(executor.getProvider(), "qwen-web");
|
||||
});
|
||||
|
||||
// ── HuggingChat Execution Tests ──────────────────────────────────────────────
|
||||
|
||||
test("HuggingChat: streaming returns SSE chunks", async () => {
|
||||
|
||||
Reference in New Issue
Block a user