qwenproxy-cli 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +14 -14
- package/README.md +906 -906
- package/bin/qwenproxy.js +5 -1
- package/package.json +78 -78
- package/src/api/error-classifier.ts +159 -159
- package/src/api/error-helpers.ts +118 -118
- package/src/api/models.ts +261 -261
- package/src/api/server.ts +860 -859
- package/src/cache/memory-cache.ts +385 -385
- package/src/clean-cache.ts +204 -204
- package/src/core/account-concurrency.ts +671 -671
- package/src/core/account-manager.ts +301 -297
- package/src/core/account-priority.ts +163 -163
- package/src/core/accounts.ts +186 -186
- package/src/core/config.ts +383 -383
- package/src/core/crypto-utils.ts +79 -79
- package/src/core/database.ts +276 -276
- package/src/core/errors.ts +118 -118
- package/src/core/logger.ts +269 -269
- package/src/core/memory-usage.ts +84 -84
- package/src/core/metrics.ts +291 -291
- package/src/core/model-alias.ts +77 -77
- package/src/core/model-registry.ts +544 -544
- package/src/core/mutex.ts +119 -119
- package/src/core/paths.ts +199 -199
- package/src/core/prompt-limits.ts +214 -214
- package/src/core/reasoning-effort.ts +102 -102
- package/src/core/stream-registry.ts +96 -96
- package/src/core/waf-isolation.ts +117 -117
- package/src/core/watchdog.ts +195 -195
- package/src/delete-chats.ts +23 -23
- package/src/index.ts +65 -64
- package/src/login.ts +147 -147
- package/src/reset-cooldowns.ts +11 -11
- package/src/routes/anthropic/index.ts +355 -355
- package/src/routes/anthropic/translate.ts +522 -522
- package/src/routes/anthropic/types.ts +154 -154
- package/src/routes/anthropic/validation.ts +144 -144
- package/src/routes/chat/account.ts +1817 -1817
- package/src/routes/chat/context.ts +241 -241
- package/src/routes/chat/errors.ts +85 -85
- package/src/routes/chat/helpers.ts +268 -268
- package/src/routes/chat/index.ts +618 -618
- package/src/routes/chat/media.ts +285 -285
- package/src/routes/chat/retry-policy.ts +754 -754
- package/src/routes/chat/stop.ts +98 -98
- package/src/routes/chat/streaming.ts +2710 -2710
- package/src/routes/chat/validation.ts +526 -526
- package/src/routes/chat.ts +2 -2
- package/src/routes/completions.ts +290 -290
- package/src/routes/images.ts +139 -139
- package/src/routes/responses/adapter.ts +503 -503
- package/src/routes/responses/index.ts +405 -405
- package/src/routes/responses/state.ts +230 -230
- package/src/routes/responses/streaming.ts +528 -528
- package/src/routes/responses/types.ts +285 -285
- package/src/routes/responses/validation.ts +202 -202
- package/src/routes/upload.ts +731 -731
- package/src/routes/videos.ts +214 -214
- package/src/services/auth-playwright.ts +173 -173
- package/src/services/captcha-coordinator.ts +161 -161
- package/src/services/captcha-solver.ts +553 -553
- package/src/services/chat-cleanup.ts +80 -80
- package/src/services/context-meter.ts +317 -317
- package/src/services/fingerprint.ts +242 -242
- package/src/services/human-behavior.ts +173 -173
- package/src/services/media-generation.ts +1748 -1748
- package/src/services/playwright.ts +2800 -2800
- package/src/services/qwen-chat-pool.ts +345 -345
- package/src/services/qwen-errors.ts +133 -133
- package/src/services/qwen-headers.ts +79 -79
- package/src/services/qwen-thread-state.ts +393 -393
- package/src/services/qwen-url.ts +19 -19
- package/src/services/qwen.ts +3126 -3126
- package/src/services/session-keeper.ts +88 -88
- package/src/services/token-estimation-metrics.ts +118 -118
- package/src/sync/claude-code.ts +75 -75
- package/src/sync/codex.ts +123 -123
- package/src/sync/index.ts +362 -362
- package/src/sync/omp.ts +105 -105
- package/src/sync/opencode.ts +214 -214
- package/src/sync/types.ts +53 -53
- package/src/sync/utils.ts +27 -27
- package/src/sync-clients.ts +189 -189
- package/src/tools/instructions.ts +137 -137
- package/src/tools/manifest.ts +81 -81
- package/src/tools/parser.ts +2989 -2989
- package/src/tools/toolcall-tags.ts +142 -142
- package/src/tui/app.ts +264 -264
- package/src/tui/index.ts +61 -61
- package/src/tui/markdown.ts +258 -258
- package/src/tui/proxy-client.ts +331 -326
- package/src/tui/screen.ts +294 -278
- package/src/tui/server-manager.ts +270 -270
- package/src/tui/theme.ts +432 -432
- package/src/tui/types.ts +33 -33
- package/src/tui/views/accounts-view.ts +656 -656
- package/src/tui/views/chat-view.ts +1018 -823
- package/src/tui/views/logs-view.ts +479 -413
- package/src/tui/views/status-view.ts +204 -204
- package/src/tui/views/storage-view.ts +304 -291
- package/src/tui/views/sync-view.ts +409 -409
- package/src/types/ali-oss.d.ts +32 -32
- package/src/utils/context-truncation.ts +84 -84
- package/src/utils/json.ts +380 -380
- package/src/utils/session-id.ts +37 -37
- package/src/utils/tool-call-guard.ts +84 -84
- package/src/utils/types.ts +109 -109
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
import {
|
|
2
|
-
QwenProxyError,
|
|
3
|
-
UpstreamRateLimit,
|
|
4
|
-
UpstreamError,
|
|
5
|
-
AuthError,
|
|
6
|
-
} from "../core/errors.ts";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Messages from the completion fetch that indicate a transient network or
|
|
10
|
-
* first-byte stall. These must be treated as retryable so tryCreateStreamWithRetry
|
|
11
|
-
* rotates to another account instead of failing with a terminal 500 that the
|
|
12
|
-
* client has to retry manually (which looks like an infinite hang).
|
|
13
|
-
*/
|
|
14
|
-
export function isRetryableFetchErrorMessage(message: string): boolean {
|
|
15
|
-
return (
|
|
16
|
-
message.includes("fetch failed") ||
|
|
17
|
-
message.includes("ECONNREFUSED") ||
|
|
18
|
-
message.includes("ETIMEDOUT") ||
|
|
19
|
-
message.includes("etimedout") ||
|
|
20
|
-
message.includes("ENOTFOUND") ||
|
|
21
|
-
message.includes("network") ||
|
|
22
|
-
message.includes("timed out waiting for response headers")
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class RetryableQwenStreamError extends UpstreamRateLimit {
|
|
27
|
-
readonly retryAfterMs: number;
|
|
28
|
-
/** Original upstream category; avoids exposing the inherited rate-limit code. */
|
|
29
|
-
upstreamCode?: string;
|
|
30
|
-
|
|
31
|
-
constructor(message: string, retryAfterMs: number) {
|
|
32
|
-
super(message);
|
|
33
|
-
this.name = "RetryableQwenStreamError";
|
|
34
|
-
this.retryAfterMs = retryAfterMs;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The account-level personalization sync could not be confirmed. Agent
|
|
40
|
-
* instructions ride ONLY the personalization channel (never inline in the
|
|
41
|
-
* prompt), so an unconfirmed sync must fail the attempt — the retry policy
|
|
42
|
-
* rotates to another account — instead of sending a request the model would
|
|
43
|
-
* answer without any instructions or tools. Surfaces as 503 (service
|
|
44
|
-
* degraded) if every account fails to sync.
|
|
45
|
-
*/
|
|
46
|
-
export class PersonalizationSyncError extends QwenProxyError {
|
|
47
|
-
readonly statusCode = 503;
|
|
48
|
-
readonly type = "service_unavailable";
|
|
49
|
-
readonly code = "personalization_unavailable";
|
|
50
|
-
|
|
51
|
-
constructor(message: string) {
|
|
52
|
-
super(message);
|
|
53
|
-
this.name = "PersonalizationSyncError";
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export class QwenUpstreamError extends UpstreamError {
|
|
58
|
-
readonly upstreamCode: string;
|
|
59
|
-
readonly upstreamStatus: number;
|
|
60
|
-
|
|
61
|
-
constructor(message: string, upstreamCode: string, upstreamStatus: number) {
|
|
62
|
-
super(message);
|
|
63
|
-
this.name = "QwenUpstreamError";
|
|
64
|
-
this.upstreamCode = upstreamCode;
|
|
65
|
-
this.upstreamStatus = upstreamStatus;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export class QwenSessionExpiredError extends AuthError {
|
|
70
|
-
readonly accountId: string;
|
|
71
|
-
|
|
72
|
-
constructor(message: string, accountId: string) {
|
|
73
|
-
super(message);
|
|
74
|
-
this.name = "QwenSessionExpiredError";
|
|
75
|
-
this.accountId = accountId;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export class QwenUpstreamUnavailableError extends RetryableQwenStreamError {
|
|
80
|
-
readonly httpStatusCode: number;
|
|
81
|
-
|
|
82
|
-
constructor(message: string, httpStatusCode: number) {
|
|
83
|
-
super(message, 5000);
|
|
84
|
-
this.name = "QwenUpstreamUnavailableError";
|
|
85
|
-
this.upstreamCode = "upstream_unavailable";
|
|
86
|
-
this.httpStatusCode = httpStatusCode;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export class QwenNetworkError extends RetryableQwenStreamError {
|
|
91
|
-
constructor(message: string) {
|
|
92
|
-
super(message, 3000);
|
|
93
|
-
this.name = "QwenNetworkError";
|
|
94
|
-
this.upstreamCode = "network_error";
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Return the meaningful upstream category for logs and OpenAI error payloads.
|
|
100
|
-
* RetryableQwenStreamError inherits the rate-limit base class for legacy retry
|
|
101
|
-
* handling, so its inherited `code` must not be used as the actual category.
|
|
102
|
-
*/
|
|
103
|
-
export function getQwenErrorCode(error: unknown): string | undefined {
|
|
104
|
-
if (!error || typeof error !== "object") return undefined;
|
|
105
|
-
|
|
106
|
-
const typed = error as {
|
|
107
|
-
upstreamCode?: unknown;
|
|
108
|
-
code?: unknown;
|
|
109
|
-
message?: unknown;
|
|
110
|
-
};
|
|
111
|
-
if (typeof typed.upstreamCode === "string" && typed.upstreamCode) {
|
|
112
|
-
return typed.upstreamCode;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (error instanceof QwenNetworkError) return "network_error";
|
|
116
|
-
if (error instanceof QwenUpstreamUnavailableError) {
|
|
117
|
-
return "upstream_unavailable";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (error instanceof RetryableQwenStreamError) {
|
|
121
|
-
const message = typeof typed.message === "string" ? typed.message.toLowerCase() : "";
|
|
122
|
-
if (message.includes("chat is in progress")) return "chat_in_progress";
|
|
123
|
-
if (message.includes("not exist") || message.includes("does not exist")) {
|
|
124
|
-
return "chat_not_exist";
|
|
125
|
-
}
|
|
126
|
-
if (message.includes("anti-bot") || message.includes("captcha")) {
|
|
127
|
-
return "waf_challenge";
|
|
128
|
-
}
|
|
129
|
-
return "upstream_retryable";
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return typeof typed.code === "string" && typed.code ? typed.code : undefined;
|
|
133
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
QwenProxyError,
|
|
3
|
+
UpstreamRateLimit,
|
|
4
|
+
UpstreamError,
|
|
5
|
+
AuthError,
|
|
6
|
+
} from "../core/errors.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Messages from the completion fetch that indicate a transient network or
|
|
10
|
+
* first-byte stall. These must be treated as retryable so tryCreateStreamWithRetry
|
|
11
|
+
* rotates to another account instead of failing with a terminal 500 that the
|
|
12
|
+
* client has to retry manually (which looks like an infinite hang).
|
|
13
|
+
*/
|
|
14
|
+
export function isRetryableFetchErrorMessage(message: string): boolean {
|
|
15
|
+
return (
|
|
16
|
+
message.includes("fetch failed") ||
|
|
17
|
+
message.includes("ECONNREFUSED") ||
|
|
18
|
+
message.includes("ETIMEDOUT") ||
|
|
19
|
+
message.includes("etimedout") ||
|
|
20
|
+
message.includes("ENOTFOUND") ||
|
|
21
|
+
message.includes("network") ||
|
|
22
|
+
message.includes("timed out waiting for response headers")
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class RetryableQwenStreamError extends UpstreamRateLimit {
|
|
27
|
+
readonly retryAfterMs: number;
|
|
28
|
+
/** Original upstream category; avoids exposing the inherited rate-limit code. */
|
|
29
|
+
upstreamCode?: string;
|
|
30
|
+
|
|
31
|
+
constructor(message: string, retryAfterMs: number) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "RetryableQwenStreamError";
|
|
34
|
+
this.retryAfterMs = retryAfterMs;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The account-level personalization sync could not be confirmed. Agent
|
|
40
|
+
* instructions ride ONLY the personalization channel (never inline in the
|
|
41
|
+
* prompt), so an unconfirmed sync must fail the attempt — the retry policy
|
|
42
|
+
* rotates to another account — instead of sending a request the model would
|
|
43
|
+
* answer without any instructions or tools. Surfaces as 503 (service
|
|
44
|
+
* degraded) if every account fails to sync.
|
|
45
|
+
*/
|
|
46
|
+
export class PersonalizationSyncError extends QwenProxyError {
|
|
47
|
+
readonly statusCode = 503;
|
|
48
|
+
readonly type = "service_unavailable";
|
|
49
|
+
readonly code = "personalization_unavailable";
|
|
50
|
+
|
|
51
|
+
constructor(message: string) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "PersonalizationSyncError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class QwenUpstreamError extends UpstreamError {
|
|
58
|
+
readonly upstreamCode: string;
|
|
59
|
+
readonly upstreamStatus: number;
|
|
60
|
+
|
|
61
|
+
constructor(message: string, upstreamCode: string, upstreamStatus: number) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = "QwenUpstreamError";
|
|
64
|
+
this.upstreamCode = upstreamCode;
|
|
65
|
+
this.upstreamStatus = upstreamStatus;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class QwenSessionExpiredError extends AuthError {
|
|
70
|
+
readonly accountId: string;
|
|
71
|
+
|
|
72
|
+
constructor(message: string, accountId: string) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "QwenSessionExpiredError";
|
|
75
|
+
this.accountId = accountId;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class QwenUpstreamUnavailableError extends RetryableQwenStreamError {
|
|
80
|
+
readonly httpStatusCode: number;
|
|
81
|
+
|
|
82
|
+
constructor(message: string, httpStatusCode: number) {
|
|
83
|
+
super(message, 5000);
|
|
84
|
+
this.name = "QwenUpstreamUnavailableError";
|
|
85
|
+
this.upstreamCode = "upstream_unavailable";
|
|
86
|
+
this.httpStatusCode = httpStatusCode;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class QwenNetworkError extends RetryableQwenStreamError {
|
|
91
|
+
constructor(message: string) {
|
|
92
|
+
super(message, 3000);
|
|
93
|
+
this.name = "QwenNetworkError";
|
|
94
|
+
this.upstreamCode = "network_error";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Return the meaningful upstream category for logs and OpenAI error payloads.
|
|
100
|
+
* RetryableQwenStreamError inherits the rate-limit base class for legacy retry
|
|
101
|
+
* handling, so its inherited `code` must not be used as the actual category.
|
|
102
|
+
*/
|
|
103
|
+
export function getQwenErrorCode(error: unknown): string | undefined {
|
|
104
|
+
if (!error || typeof error !== "object") return undefined;
|
|
105
|
+
|
|
106
|
+
const typed = error as {
|
|
107
|
+
upstreamCode?: unknown;
|
|
108
|
+
code?: unknown;
|
|
109
|
+
message?: unknown;
|
|
110
|
+
};
|
|
111
|
+
if (typeof typed.upstreamCode === "string" && typed.upstreamCode) {
|
|
112
|
+
return typed.upstreamCode;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (error instanceof QwenNetworkError) return "network_error";
|
|
116
|
+
if (error instanceof QwenUpstreamUnavailableError) {
|
|
117
|
+
return "upstream_unavailable";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (error instanceof RetryableQwenStreamError) {
|
|
121
|
+
const message = typeof typed.message === "string" ? typed.message.toLowerCase() : "";
|
|
122
|
+
if (message.includes("chat is in progress")) return "chat_in_progress";
|
|
123
|
+
if (message.includes("not exist") || message.includes("does not exist")) {
|
|
124
|
+
return "chat_not_exist";
|
|
125
|
+
}
|
|
126
|
+
if (message.includes("anti-bot") || message.includes("captcha")) {
|
|
127
|
+
return "waf_challenge";
|
|
128
|
+
}
|
|
129
|
+
return "upstream_retryable";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return typeof typed.code === "string" && typed.code ? typed.code : undefined;
|
|
133
|
+
}
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from "uuid";
|
|
2
|
-
import { qwenUrl, qwenOrigin } from "./qwen-url.ts";
|
|
3
|
-
import { config } from "../core/config.js";
|
|
4
|
-
let dynamicWebVersion: string | null = null;
|
|
5
|
-
|
|
6
|
-
export function updateQwenWebVersion(version?: string | null): void {
|
|
7
|
-
if (version && typeof version === "string" && /^\d+\.\d+\.\d+/.test(version)) {
|
|
8
|
-
dynamicWebVersion = version.trim();
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function getQwenWebVersion(): string {
|
|
13
|
-
return dynamicWebVersion || config.qwen.webVersion;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const QWEN_WEB_VERSION = config.qwen.webVersion;
|
|
17
|
-
export const DEFAULT_QWEN_USER_AGENT =
|
|
18
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36";
|
|
19
|
-
const QWEN_TIMEZONE_HEADER = new Date().toString().split(" (")[0];
|
|
20
|
-
|
|
21
|
-
export interface BuildQwenHeadersOptions {
|
|
22
|
-
cookie: string;
|
|
23
|
-
userAgent?: string;
|
|
24
|
-
bxUa?: string;
|
|
25
|
-
bxUmidtoken?: string;
|
|
26
|
-
bxV?: string;
|
|
27
|
-
chatSessionId?: string | null;
|
|
28
|
-
/** Real browser client-hints captured by getBasicHeaders (anti-hardcoded). */
|
|
29
|
-
secChUa?: string;
|
|
30
|
-
secChUaMobile?: string;
|
|
31
|
-
secChUaPlatform?: string;
|
|
32
|
-
/** Real web bundle version captured from the browser request (anti-hardcoded). */
|
|
33
|
-
version?: string;
|
|
34
|
-
extra?: Record<string, string>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function buildQwenRequestHeaders(
|
|
38
|
-
opts: BuildQwenHeadersOptions,
|
|
39
|
-
): Record<string, string> {
|
|
40
|
-
const headers: Record<string, string> = {
|
|
41
|
-
...(opts.extra ?? {}),
|
|
42
|
-
Accept: "application/json",
|
|
43
|
-
"Accept-Language": "pt-BR,pt;q=0.9",
|
|
44
|
-
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
45
|
-
"Content-Type": "application/json",
|
|
46
|
-
Cookie: opts.cookie,
|
|
47
|
-
Origin: qwenOrigin(),
|
|
48
|
-
Referer:
|
|
49
|
-
opts.extra?.Referer ??
|
|
50
|
-
(opts.chatSessionId
|
|
51
|
-
? qwenUrl(`/c/${encodeURIComponent(opts.chatSessionId)}`)
|
|
52
|
-
: qwenUrl("/")),
|
|
53
|
-
"Sec-Fetch-Dest": "empty",
|
|
54
|
-
"Sec-Fetch-Mode": "cors",
|
|
55
|
-
"Sec-Fetch-Site": "same-origin",
|
|
56
|
-
Connection: "keep-alive",
|
|
57
|
-
"User-Agent": opts.userAgent || DEFAULT_QWEN_USER_AGENT,
|
|
58
|
-
"X-Request-Id": uuidv4(),
|
|
59
|
-
"bx-v": opts.bxV || "2.5.37",
|
|
60
|
-
source: "web",
|
|
61
|
-
version: opts.version || getQwenWebVersion(),
|
|
62
|
-
timezone: opts.extra?.timezone || new Date().toString().split(" (")[0],
|
|
63
|
-
// Use the real browser client-hints when captured (anti-hardcoded); fall
|
|
64
|
-
// back to the static fingerprint otherwise.
|
|
65
|
-
"sec-ch-ua": opts.secChUa || '"Google Chrome";v="150", "Chromium";v="150", "Not.A/Brand";v="99"',
|
|
66
|
-
"sec-ch-ua-mobile": opts.secChUaMobile || "?0",
|
|
67
|
-
"sec-ch-ua-platform": opts.secChUaPlatform || '"Windows"',
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// The real chat.qwen.ai client sends ONLY bx-v on API requests — the WAF
|
|
71
|
-
// carries bx-ua/bx-umidtoken as browser cookies, not headers. Match that
|
|
72
|
-
// unless QWEN_SEND_BX_UA=true restores the legacy injection.
|
|
73
|
-
if (config.qwen.sendBxUa) {
|
|
74
|
-
if (opts.bxUa) headers["bx-ua"] = opts.bxUa;
|
|
75
|
-
if (opts.bxUmidtoken) headers["bx-umidtoken"] = opts.bxUmidtoken;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return headers;
|
|
79
|
-
}
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
import { qwenUrl, qwenOrigin } from "./qwen-url.ts";
|
|
3
|
+
import { config } from "../core/config.js";
|
|
4
|
+
let dynamicWebVersion: string | null = null;
|
|
5
|
+
|
|
6
|
+
export function updateQwenWebVersion(version?: string | null): void {
|
|
7
|
+
if (version && typeof version === "string" && /^\d+\.\d+\.\d+/.test(version)) {
|
|
8
|
+
dynamicWebVersion = version.trim();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getQwenWebVersion(): string {
|
|
13
|
+
return dynamicWebVersion || config.qwen.webVersion;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const QWEN_WEB_VERSION = config.qwen.webVersion;
|
|
17
|
+
export const DEFAULT_QWEN_USER_AGENT =
|
|
18
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36";
|
|
19
|
+
const QWEN_TIMEZONE_HEADER = new Date().toString().split(" (")[0];
|
|
20
|
+
|
|
21
|
+
export interface BuildQwenHeadersOptions {
|
|
22
|
+
cookie: string;
|
|
23
|
+
userAgent?: string;
|
|
24
|
+
bxUa?: string;
|
|
25
|
+
bxUmidtoken?: string;
|
|
26
|
+
bxV?: string;
|
|
27
|
+
chatSessionId?: string | null;
|
|
28
|
+
/** Real browser client-hints captured by getBasicHeaders (anti-hardcoded). */
|
|
29
|
+
secChUa?: string;
|
|
30
|
+
secChUaMobile?: string;
|
|
31
|
+
secChUaPlatform?: string;
|
|
32
|
+
/** Real web bundle version captured from the browser request (anti-hardcoded). */
|
|
33
|
+
version?: string;
|
|
34
|
+
extra?: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildQwenRequestHeaders(
|
|
38
|
+
opts: BuildQwenHeadersOptions,
|
|
39
|
+
): Record<string, string> {
|
|
40
|
+
const headers: Record<string, string> = {
|
|
41
|
+
...(opts.extra ?? {}),
|
|
42
|
+
Accept: "application/json",
|
|
43
|
+
"Accept-Language": "pt-BR,pt;q=0.9",
|
|
44
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
Cookie: opts.cookie,
|
|
47
|
+
Origin: qwenOrigin(),
|
|
48
|
+
Referer:
|
|
49
|
+
opts.extra?.Referer ??
|
|
50
|
+
(opts.chatSessionId
|
|
51
|
+
? qwenUrl(`/c/${encodeURIComponent(opts.chatSessionId)}`)
|
|
52
|
+
: qwenUrl("/")),
|
|
53
|
+
"Sec-Fetch-Dest": "empty",
|
|
54
|
+
"Sec-Fetch-Mode": "cors",
|
|
55
|
+
"Sec-Fetch-Site": "same-origin",
|
|
56
|
+
Connection: "keep-alive",
|
|
57
|
+
"User-Agent": opts.userAgent || DEFAULT_QWEN_USER_AGENT,
|
|
58
|
+
"X-Request-Id": uuidv4(),
|
|
59
|
+
"bx-v": opts.bxV || "2.5.37",
|
|
60
|
+
source: "web",
|
|
61
|
+
version: opts.version || getQwenWebVersion(),
|
|
62
|
+
timezone: opts.extra?.timezone || new Date().toString().split(" (")[0],
|
|
63
|
+
// Use the real browser client-hints when captured (anti-hardcoded); fall
|
|
64
|
+
// back to the static fingerprint otherwise.
|
|
65
|
+
"sec-ch-ua": opts.secChUa || '"Google Chrome";v="150", "Chromium";v="150", "Not.A/Brand";v="99"',
|
|
66
|
+
"sec-ch-ua-mobile": opts.secChUaMobile || "?0",
|
|
67
|
+
"sec-ch-ua-platform": opts.secChUaPlatform || '"Windows"',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// The real chat.qwen.ai client sends ONLY bx-v on API requests — the WAF
|
|
71
|
+
// carries bx-ua/bx-umidtoken as browser cookies, not headers. Match that
|
|
72
|
+
// unless QWEN_SEND_BX_UA=true restores the legacy injection.
|
|
73
|
+
if (config.qwen.sendBxUa) {
|
|
74
|
+
if (opts.bxUa) headers["bx-ua"] = opts.bxUa;
|
|
75
|
+
if (opts.bxUmidtoken) headers["bx-umidtoken"] = opts.bxUmidtoken;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return headers;
|
|
79
|
+
}
|