qwenproxy-cli 1.0.0 → 1.0.2
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 +77 -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 +2878 -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 +259 -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/update-cli.ts +121 -0
- 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
package/src/core/config.ts
CHANGED
|
@@ -1,383 +1,383 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
const envSchema = z
|
|
4
|
-
.object({
|
|
5
|
-
PORT: z
|
|
6
|
-
.string()
|
|
7
|
-
.regex(/^\d+$/, "PORT must be a number")
|
|
8
|
-
.refine((value) => {
|
|
9
|
-
const port = Number(value);
|
|
10
|
-
return port >= 1 && port <= 65535;
|
|
11
|
-
}, "PORT must be between 1 and 65535")
|
|
12
|
-
.default("7936"),
|
|
13
|
-
HOST: z.string().default("0.0.0.0"),
|
|
14
|
-
INTERNAL_HOST: z.string().default("127.0.0.1"),
|
|
15
|
-
USER_AGENT: z
|
|
16
|
-
.string()
|
|
17
|
-
.default(
|
|
18
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
|
|
19
|
-
),
|
|
20
|
-
QWEN_BX_V: z.string().default("2.5.37"),
|
|
21
|
-
// Version header on Qwen API requests = the deployed web bundle version
|
|
22
|
-
// (fallback when dynamic DOM detection is not yet completed).
|
|
23
|
-
// Override via env when Qwen ships a new bundle.
|
|
24
|
-
QWEN_WEB_VERSION: z.string().default("0.2.91"),
|
|
25
|
-
// Controls bx-ua/bx-umidtoken injection on the GENERAL API paths
|
|
26
|
-
// (chats/new, settings): those work without them (live-probed). The
|
|
27
|
-
// completions path is the exception — the 0.2.86 HAR shows the real
|
|
28
|
-
// client POSTs completions WITH bx-ua, so it always includes the captured
|
|
29
|
-
// tokens regardless of this flag (buildCompletionHeaders). Set true
|
|
30
|
-
// to inject them everywhere (legacy behavior).
|
|
31
|
-
QWEN_SEND_BX_UA: z.string().default("false"),
|
|
32
|
-
// Conversation mode: "thread" (default) reuses the upstream Qwen chat via
|
|
33
|
-
// parent_id and sends the thread-native delta; "temp" creates a NEW Qwen
|
|
34
|
-
// temp chat (chat_mode:"local") for every request and sends the full
|
|
35
|
-
// history inline (OpenAI standard). Temp chats are ephemeral and never
|
|
36
|
-
// appear in the account's chat list (live-probed).
|
|
37
|
-
QWEN_CHAT_MODE: z.enum(["thread", "temp"]).default("thread"),
|
|
38
|
-
PLAYWRIGHT_HEADLESS: z.string().default("true"),
|
|
39
|
-
PLAYWRIGHT_BROWSER: z
|
|
40
|
-
.enum(["chromium", "chrome", "edge"])
|
|
41
|
-
.default("chromium"),
|
|
42
|
-
PLAYWRIGHT_INIT_BATCH_SIZE: z.string().default("1"),
|
|
43
|
-
PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS: z.string().default("10000"),
|
|
44
|
-
PLAYWRIGHT_IDLE_CONTEXT_TTL_MS: z.string().default("60000"),
|
|
45
|
-
PLAYWRIGHT_JS_HEAP_MB: z.string().default("256"),
|
|
46
|
-
PLAYWRIGHT_LOW_MEMORY_FLAGS: z.string().default("true"),
|
|
47
|
-
// Keep 2 warm contexts by default ({main + reserve} covers the common
|
|
48
|
-
// failover hop without opening one browser per account): after warmup two
|
|
49
|
-
// browsers stay open; any extra context (simultaneous use / failover) is
|
|
50
|
-
// closed once idle. The cap only evicts IDLE contexts — busy mutexes and
|
|
51
|
-
// active streams are never touched, so concurrent accounts each keep their
|
|
52
|
-
// own context while serving. Accounts in cooldown (rate-limited) sit idle,
|
|
53
|
-
// drop out of the warm set and get evicted.
|
|
54
|
-
PLAYWRIGHT_MAX_ACTIVE_CONTEXTS: z.string().default("2"),
|
|
55
|
-
PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("
|
|
56
|
-
CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
|
|
57
|
-
CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
|
|
58
|
-
CAPTCHA_SOLVER_TIMEOUT_MS: z.string().default("15000"),
|
|
59
|
-
CAPTCHA_SOLVER_RETRY_DELAY_MS: z.string().default("1000"),
|
|
60
|
-
CAPTCHA_SOLVER_SETTLE_MS: z.string().default("2000"),
|
|
61
|
-
CAPTCHA_ACCOUNT_COOLDOWN_MS: z.string().default("120000"),
|
|
62
|
-
// Cap for the escalating quarantine applied when a challenge could NOT be
|
|
63
|
-
// solved (hard block). Each consecutive hard block doubles the window
|
|
64
|
-
// (base = CAPTCHA_ACCOUNT_COOLDOWN_MS) up to this cap.
|
|
65
|
-
CAPTCHA_HARD_BLOCK_MAX_COOLDOWN_MS: z.string().default("3600000"),
|
|
66
|
-
OSS_MULTIPART_THRESHOLD_MB: z.string().default("5"),
|
|
67
|
-
CHAT_REQUEST_LOG: z.string().default("false"),
|
|
68
|
-
HTTP_TIMEOUT: z.string().default("15000"),
|
|
69
|
-
CHAT_TIMEOUT: z.string().default("180000"),
|
|
70
|
-
NAVIGATION_TIMEOUT: z.string().default("60000"),
|
|
71
|
-
PAGE_TIMEOUT: z.string().default("60000"),
|
|
72
|
-
HEADERS_TIMEOUT: z.string().default("90000"),
|
|
73
|
-
TIME_TO_FIRST_BYTE: z.string().default("60000"),
|
|
74
|
-
IDLE_STREAM_TIMEOUT: z.string().default("60000"),
|
|
75
|
-
// Deadline for the FIRST upstream chunk on thinking models (the reasoning
|
|
76
|
-
// idle of 600s is for gaps AFTER data flows; a stream that produced
|
|
77
|
-
// nothing in this window is dead and should fail fast, retryable).
|
|
78
|
-
QWEN_FIRST_CHUNK_TIMEOUT: z.string().default("180000"),
|
|
79
|
-
TOTAL_REQUEST_TIMEOUT: z.string().default("600000"),
|
|
80
|
-
// Mid-stream silence window for thinking models: 3 min with ZERO upstream
|
|
81
|
-
// bytes is a dead stream (WAF swallow / dropped connection) — fail fast and
|
|
82
|
-
// let the retry policy rotate accounts. Flowing reasoning chunks RESET this
|
|
83
|
-
// timer, so legitimate slow thinking is never cut; only total silence is.
|
|
84
|
-
REASONING_MODEL_TIMEOUT: z.string().default("180000"),
|
|
85
|
-
CACHE_TTL: z.string().default("3600"),
|
|
86
|
-
RESPONSE_TTL: z.string().default("1800"),
|
|
87
|
-
CACHE_COMPRESSION_ENABLED: z.string().default("true"),
|
|
88
|
-
CACHE_COMPRESSION_THRESHOLD: z.string().default("1024"),
|
|
89
|
-
CACHE_COMPRESSION_LEVEL: z.string().default("6"),
|
|
90
|
-
METRICS_INTERVAL: z.string().default("10000"),
|
|
91
|
-
WATCHDOG_INTERVAL: z.string().default("5000"),
|
|
92
|
-
WATCHDOG_FAILURES: z.string().default("3"),
|
|
93
|
-
RAM_WARNING: z.string().default("80"),
|
|
94
|
-
RAM_CRITICAL: z.string().default("95"),
|
|
95
|
-
WS_WARNING: z.string().default("50"),
|
|
96
|
-
WS_CRITICAL: z.string().default("100"),
|
|
97
|
-
RETRY_BASE_DELAY_MS: z.string().default(process.env.TEST_MOCK_QWEN_AUTH === "true" ? "50" : "1000"),
|
|
98
|
-
RETRY_MAX_DELAY_MS: z.string().default(process.env.TEST_MOCK_QWEN_AUTH === "true" ? "200" : "10000"),
|
|
99
|
-
RETRY_MAX_ATTEMPTS: z.string().default("3"),
|
|
100
|
-
RETRY_MAX_ACCOUNT_SWITCHES: z.string().default("2"),
|
|
101
|
-
RETRY_ON_UNKNOWN_UPSTREAM: z.string().default("true"),
|
|
102
|
-
RETRY_AUTO_MALFORMED_TOOLS: z.string().default("true"),
|
|
103
|
-
RETRY_AUTO_MALFORMED_TOOLS_MAX: z.string().default("2"),
|
|
104
|
-
MAX_TOOL_CALLS_PER_TURN: z.string().default("24"),
|
|
105
|
-
QWEN_REPEATED_TOOL_CALL_WARN: z.string().default("2"),
|
|
106
|
-
ACCOUNT_MAX_CONCURRENT_STREAMS: z.string().default("2"),
|
|
107
|
-
ACCOUNT_BUSY_WAIT_MS: z.string().default("30000"),
|
|
108
|
-
// Cap for the "wait forever" account-lease queue (thread owner / last
|
|
109
|
-
// usable account): the queue previously had NO deadline, so a stuck lease
|
|
110
|
-
// holder made the next turn wait up to ~600s. A generous finite cap keeps
|
|
111
|
-
// the wait far above the normal 30s but still bounded.
|
|
112
|
-
ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS: z.string().default("120000"),
|
|
113
|
-
// Hard deadline for one stream-acquire attempt. A dead account (upstream
|
|
114
|
-
// swallows the completion fetch) otherwise chains metadata/header timeouts
|
|
115
|
-
// for minutes; this fails the attempt visibly so the retry loop switches.
|
|
116
|
-
ACQUIRE_DEADLINE_MS: z.string().default("120000"),
|
|
117
|
-
ACCOUNT_LEASE_MAX_DURATION_MS: z.string().default("600000"),
|
|
118
|
-
ACCOUNT_INIT_FAILURE_COOLDOWN_MS: z.string().default("300000"),
|
|
119
|
-
// Timeout before a request waiting on the CHAT lock gives up. The chat lock
|
|
120
|
-
// is held for the entire stream lifetime, so it must cover the longest
|
|
121
|
-
// legitimate generation (reasoning models with huge contexts can spend
|
|
122
|
-
// 2-3 min producing the first byte chain). A 60s hard cap turned a normal
|
|
123
|
-
// long turn into a 500 for every concurrent request on the same chat
|
|
124
|
-
// (2026-08-22 production log: lock held 130s -> acquire_deadline 120s).
|
|
125
|
-
CHAT_LOCK_TIMEOUT_MS: z
|
|
126
|
-
.string()
|
|
127
|
-
.regex(/^\d+$/, "CHAT_LOCK_TIMEOUT_MS must be a number")
|
|
128
|
-
.default("180000"),
|
|
129
|
-
STREAM_DISCONNECT_GRACE_MS: z
|
|
130
|
-
.string()
|
|
131
|
-
.regex(/^\d+$/, "STREAM_DISCONNECT_GRACE_MS must be a number")
|
|
132
|
-
.default("4000"),
|
|
133
|
-
CHAT_IN_PROGRESS_RETRY_DELAY_MS: z.string().default("2000"),
|
|
134
|
-
// Temporarily-busy window after a chat_in_progress: long enough to absorb
|
|
135
|
-
// the upstream chat settle (measured ~1-2s), short enough that the next
|
|
136
|
-
// turn of the sticky owner is not pushed to a cold account with a full
|
|
137
|
-
// context replay (8s caused a needless 13.3s hop in the 20:04 session).
|
|
138
|
-
CHAT_IN_PROGRESS_BUSY_MS: z.string().default("4000"),
|
|
139
|
-
// Same-chat retry budget for chat_in_progress. The upstream chat stays "in
|
|
140
|
-
// progress" for 2-16s after a completed turn (grows with turn size); each
|
|
141
|
-
// retry waits a jittered window (busyMs-based) and NO retry re-sends the
|
|
142
|
-
// full context — the escalation (new chat + full replay on a cold account)
|
|
143
|
-
// was the ~1MB re-upload that made tool loops feel like ~40 minutes. After
|
|
144
|
-
// this budget the request FAILS (thread binding kept) and the client's own
|
|
145
|
-
// retry lands on the settled chat with the delta intact.
|
|
146
|
-
CHAT_IN_PROGRESS_MAX_RETRIES: z.string().default("6"),
|
|
147
|
-
MID_STREAM_FAILOVER_THRESHOLD: z.string().default("2"),
|
|
148
|
-
MID_STREAM_FAILOVER_BUSY_MS: z.string().default("60000"),
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
QWEN_BASE_URL: z.string().default("https://chat.qwen.ai"),
|
|
152
|
-
QWEN_CHAT_POOL_SIZE: z.string().default("1"),
|
|
153
|
-
QWEN_CHAT_POOL_MODELS: z.string().default("qwen3.7-plus"),
|
|
154
|
-
QWEN_PERSONALIZATION_FROM_REQUEST: z.string().default("true"),
|
|
155
|
-
QWEN_PERSONALIZATION_VERIFY_GET: z.string().default("true"),
|
|
156
|
-
QWEN_BROWSER_ONLY_FETCH: z.string().default("true"),
|
|
157
|
-
QWEN_MAX_PROMPT_BYTES: z.string().default("0"),
|
|
158
|
-
QWEN_MAX_PERSONALIZATION_BYTES: z.string().default("200000"),
|
|
159
|
-
CONTEXT_METER_ENABLED: z.string().default("true"),
|
|
160
|
-
CONTEXT_METER_WINDOW_TOKENS: z.string().default("0"),
|
|
161
|
-
CONTEXT_METER_REPORT_USAGE: z.string().default("true"),
|
|
162
|
-
DELETE_ALL_CHATS_ON_SHUTDOWN: z.string().default("false"),
|
|
163
|
-
// Keep idle account pages alive (subtlePageActivity + occasional reload).
|
|
164
|
-
// The Baxia WAF scores live page behavior (pointer/scroll events, open
|
|
165
|
-
// session) — an account whose page sits frozen for minutes returns a low
|
|
166
|
-
// trust score and gets TMD-challenged on the next request. On by default;
|
|
167
|
-
// the keeper skips accounts that are mid-stream or mutex-busy.
|
|
168
|
-
SESSION_KEEP_ALIVE_ENABLED: z.string().default("true"),
|
|
169
|
-
SESSION_KEEP_ALIVE_INTERVAL_MS: z.string().default("
|
|
170
|
-
SESSION_KEEP_ALIVE_IDLE_MS: z.string().default("120000"),
|
|
171
|
-
SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS: z.string().default("480000"),
|
|
172
|
-
API_KEY: z.string().default(""),
|
|
173
|
-
// Static x-ratelimit-* response headers (OpenAI-shaped, doc §5.2). The proxy
|
|
174
|
-
// does not enforce a token/request quota; these exist for SDK/tool parsing.
|
|
175
|
-
RATE_LIMIT_REQUESTS: z.string().default("5000"),
|
|
176
|
-
RATE_LIMIT_TOKENS: z.string().default("200000"),
|
|
177
|
-
})
|
|
178
|
-
;
|
|
179
|
-
|
|
180
|
-
const env = envSchema.parse(process.env);
|
|
181
|
-
|
|
182
|
-
export const config = {
|
|
183
|
-
server: {
|
|
184
|
-
port: parseInt(env.PORT),
|
|
185
|
-
host: env.HOST,
|
|
186
|
-
internalHost: env.INTERNAL_HOST,
|
|
187
|
-
rateLimit: {
|
|
188
|
-
requests: parseInt(env.RATE_LIMIT_REQUESTS),
|
|
189
|
-
tokens: parseInt(env.RATE_LIMIT_TOKENS),
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
logging: {
|
|
193
|
-
chatRequests: env.CHAT_REQUEST_LOG === "true",
|
|
194
|
-
},
|
|
195
|
-
auth: {
|
|
196
|
-
userAgent: env.USER_AGENT,
|
|
197
|
-
bxV: env.QWEN_BX_V,
|
|
198
|
-
},
|
|
199
|
-
playwright: {
|
|
200
|
-
headless: env.PLAYWRIGHT_HEADLESS !== "false",
|
|
201
|
-
browser: env.PLAYWRIGHT_BROWSER,
|
|
202
|
-
initBatchSize: Math.max(1, parseInt(env.PLAYWRIGHT_INIT_BATCH_SIZE)),
|
|
203
|
-
contextCloseTimeoutMs: Math.max(
|
|
204
|
-
1_000,
|
|
205
|
-
parseInt(env.PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS),
|
|
206
|
-
),
|
|
207
|
-
idleContextTtlMs: Math.max(0, parseInt(env.PLAYWRIGHT_IDLE_CONTEXT_TTL_MS)),
|
|
208
|
-
jsHeapMb: Math.max(64, parseInt(env.PLAYWRIGHT_JS_HEAP_MB)),
|
|
209
|
-
lowMemoryFlags: env.PLAYWRIGHT_LOW_MEMORY_FLAGS !== "false",
|
|
210
|
-
maxActiveContexts: Math.max(0, parseInt(env.PLAYWRIGHT_MAX_ACTIVE_CONTEXTS)),
|
|
211
|
-
prepareAllOnStartup: env.PLAYWRIGHT_PREPARE_ALL_ON_STARTUP !== "false",
|
|
212
|
-
},
|
|
213
|
-
captcha: {
|
|
214
|
-
enabled: env.CAPTCHA_SOLVER_ENABLED === "true",
|
|
215
|
-
maxAttempts: Math.max(1, Math.min(5, parseInt(env.CAPTCHA_SOLVER_MAX_ATTEMPTS))),
|
|
216
|
-
timeoutMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_TIMEOUT_MS)),
|
|
217
|
-
retryDelayMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_RETRY_DELAY_MS)),
|
|
218
|
-
settleMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_SETTLE_MS)),
|
|
219
|
-
/** Rest an account whose challenge could not be cleared before reusing it. */
|
|
220
|
-
accountCooldownMs: Math.max(0, parseInt(env.CAPTCHA_ACCOUNT_COOLDOWN_MS)),
|
|
221
|
-
/** Cap for the escalating hard-block quarantine (×2 per consecutive block). */
|
|
222
|
-
hardBlockMaxCooldownMs: Math.max(
|
|
223
|
-
0,
|
|
224
|
-
parseInt(env.CAPTCHA_HARD_BLOCK_MAX_COOLDOWN_MS),
|
|
225
|
-
),
|
|
226
|
-
},
|
|
227
|
-
oss: {
|
|
228
|
-
multipartThresholdBytes: Math.max(
|
|
229
|
-
1 * 1024 * 1024,
|
|
230
|
-
parseInt(env.OSS_MULTIPART_THRESHOLD_MB) * 1024 * 1024,
|
|
231
|
-
),
|
|
232
|
-
},
|
|
233
|
-
timeouts: {
|
|
234
|
-
http: parseInt(env.HTTP_TIMEOUT),
|
|
235
|
-
chat: parseInt(env.CHAT_TIMEOUT),
|
|
236
|
-
navigation: parseInt(env.NAVIGATION_TIMEOUT),
|
|
237
|
-
page: parseInt(env.PAGE_TIMEOUT),
|
|
238
|
-
headers: parseInt(env.HEADERS_TIMEOUT),
|
|
239
|
-
timeToFirstByte: parseInt(env.TIME_TO_FIRST_BYTE),
|
|
240
|
-
idleStreamTimeout: parseInt(env.IDLE_STREAM_TIMEOUT),
|
|
241
|
-
totalRequestTimeout: parseInt(env.TOTAL_REQUEST_TIMEOUT),
|
|
242
|
-
reasoningModelTimeout: parseInt(env.REASONING_MODEL_TIMEOUT),
|
|
243
|
-
firstChunkTimeout: parseInt(env.QWEN_FIRST_CHUNK_TIMEOUT),
|
|
244
|
-
},
|
|
245
|
-
cache: {
|
|
246
|
-
defaultTTL: parseInt(env.CACHE_TTL),
|
|
247
|
-
responseTTL: parseInt(env.RESPONSE_TTL),
|
|
248
|
-
compression: {
|
|
249
|
-
enabled: env.CACHE_COMPRESSION_ENABLED !== "false",
|
|
250
|
-
threshold: parseInt(env.CACHE_COMPRESSION_THRESHOLD),
|
|
251
|
-
level: parseInt(env.CACHE_COMPRESSION_LEVEL),
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
|
|
255
|
-
metrics: {
|
|
256
|
-
interval: parseInt(env.METRICS_INTERVAL),
|
|
257
|
-
},
|
|
258
|
-
watchdog: {
|
|
259
|
-
checkInterval: parseInt(env.WATCHDOG_INTERVAL),
|
|
260
|
-
consecutiveFailuresThreshold: parseInt(env.WATCHDOG_FAILURES),
|
|
261
|
-
ram: {
|
|
262
|
-
warningThreshold: parseInt(env.RAM_WARNING),
|
|
263
|
-
criticalThreshold: parseInt(env.RAM_CRITICAL),
|
|
264
|
-
},
|
|
265
|
-
streams: {
|
|
266
|
-
warningThreshold: parseInt(env.WS_WARNING),
|
|
267
|
-
criticalThreshold: parseInt(env.WS_CRITICAL),
|
|
268
|
-
},
|
|
269
|
-
},
|
|
270
|
-
retry: {
|
|
271
|
-
baseDelayMs: parseInt(env.RETRY_BASE_DELAY_MS),
|
|
272
|
-
maxDelayMs: parseInt(env.RETRY_MAX_DELAY_MS),
|
|
273
|
-
maxAttempts: Math.max(1, parseInt(env.RETRY_MAX_ATTEMPTS)),
|
|
274
|
-
maxAccountSwitches: Math.max(0, parseInt(env.RETRY_MAX_ACCOUNT_SWITCHES)),
|
|
275
|
-
onUnknownUpstream: env.RETRY_ON_UNKNOWN_UPSTREAM !== "false",
|
|
276
|
-
chatInProgressDelayMs: Math.max(0, parseInt(env.CHAT_IN_PROGRESS_RETRY_DELAY_MS)),
|
|
277
|
-
chatInProgressBusyMs: Math.max(0, parseInt(env.CHAT_IN_PROGRESS_BUSY_MS)),
|
|
278
|
-
chatInProgressMaxAttempts: Math.max(1, parseInt(env.CHAT_IN_PROGRESS_MAX_RETRIES)),
|
|
279
|
-
midStreamFailoverThreshold: Math.max(
|
|
280
|
-
0,
|
|
281
|
-
parseInt(env.MID_STREAM_FAILOVER_THRESHOLD),
|
|
282
|
-
),
|
|
283
|
-
midStreamFailoverBusyMs: Math.max(
|
|
284
|
-
0,
|
|
285
|
-
parseInt(env.MID_STREAM_FAILOVER_BUSY_MS),
|
|
286
|
-
),
|
|
287
|
-
autoRetryMalformedTools: env.RETRY_AUTO_MALFORMED_TOOLS !== "false",
|
|
288
|
-
autoRetryMalformedToolsMax: Math.max(1, parseInt(env.RETRY_AUTO_MALFORMED_TOOLS_MAX)),
|
|
289
|
-
maxToolCallsPerTurn: Math.max(0, parseInt(env.MAX_TOOL_CALLS_PER_TURN)),
|
|
290
|
-
repeatedToolCallWarnThreshold: Math.max(
|
|
291
|
-
1,
|
|
292
|
-
parseInt(env.QWEN_REPEATED_TOOL_CALL_WARN),
|
|
293
|
-
),
|
|
294
|
-
},
|
|
295
|
-
concurrency: {
|
|
296
|
-
maxStreamsPerAccount: Math.max(1, parseInt(env.ACCOUNT_MAX_CONCURRENT_STREAMS)),
|
|
297
|
-
busyWaitMs: Math.max(
|
|
298
|
-
0,
|
|
299
|
-
Number.isFinite(parseInt(env.ACCOUNT_BUSY_WAIT_MS as string))
|
|
300
|
-
? parseInt(env.ACCOUNT_BUSY_WAIT_MS as string)
|
|
301
|
-
: 30_000,
|
|
302
|
-
),
|
|
303
|
-
/** Bound for the "wait forever" lease queue (default 2 min). */
|
|
304
|
-
queueWaitForeverCapMs: Math.max(
|
|
305
|
-
0,
|
|
306
|
-
Number.isFinite(parseInt(env.ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS as string))
|
|
307
|
-
? parseInt(env.ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS as string)
|
|
308
|
-
: 120_000,
|
|
309
|
-
),
|
|
310
|
-
/** Hard deadline for one stream-acquire attempt (default 2 min). */
|
|
311
|
-
acquireDeadlineMs: Math.max(
|
|
312
|
-
0,
|
|
313
|
-
Number.isFinite(parseInt(env.ACQUIRE_DEADLINE_MS as string))
|
|
314
|
-
? parseInt(env.ACQUIRE_DEADLINE_MS as string)
|
|
315
|
-
: 120_000,
|
|
316
|
-
),
|
|
317
|
-
/** Safety net: force-release leases held longer than this (default 10 min). */
|
|
318
|
-
leaseMaxDurationMs: Math.max(
|
|
319
|
-
0,
|
|
320
|
-
parseInt(env.ACCOUNT_LEASE_MAX_DURATION_MS),
|
|
321
|
-
),
|
|
322
|
-
initFailureCooldownMs: Math.max(
|
|
323
|
-
30_000,
|
|
324
|
-
parseInt(env.ACCOUNT_INIT_FAILURE_COOLDOWN_MS),
|
|
325
|
-
),
|
|
326
|
-
/** Max time a request waits on the per-chat lock (default 3 min). */
|
|
327
|
-
chatLockTimeoutMs: Math.max(
|
|
328
|
-
0,
|
|
329
|
-
parseInt(env.CHAT_LOCK_TIMEOUT_MS),
|
|
330
|
-
),
|
|
331
|
-
},
|
|
332
|
-
stream: {
|
|
333
|
-
disconnectGraceMs: Math.max(
|
|
334
|
-
0,
|
|
335
|
-
parseInt(env.STREAM_DISCONNECT_GRACE_MS),
|
|
336
|
-
),
|
|
337
|
-
},
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
sessionKeeper: {
|
|
341
|
-
enabled: env.SESSION_KEEP_ALIVE_ENABLED !== "false",
|
|
342
|
-
intervalMs: parseInt(env.SESSION_KEEP_ALIVE_INTERVAL_MS),
|
|
343
|
-
idleMs: parseInt(env.SESSION_KEEP_ALIVE_IDLE_MS),
|
|
344
|
-
navigationIntervalMs: parseInt(
|
|
345
|
-
env.SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS,
|
|
346
|
-
),
|
|
347
|
-
},
|
|
348
|
-
apiKey: env.API_KEY,
|
|
349
|
-
qwen: {
|
|
350
|
-
baseUrl: env.QWEN_BASE_URL,
|
|
351
|
-
chatPoolSize: Math.max(0, parseInt(env.QWEN_CHAT_POOL_SIZE)),
|
|
352
|
-
chatPoolModels: env.QWEN_CHAT_POOL_MODELS.split(",")
|
|
353
|
-
.map((model) => model.trim())
|
|
354
|
-
.filter(Boolean),
|
|
355
|
-
personalizationFromRequest:
|
|
356
|
-
env.QWEN_PERSONALIZATION_FROM_REQUEST === "true",
|
|
357
|
-
personalizationVerifyGet: env.QWEN_PERSONALIZATION_VERIFY_GET !== "false",
|
|
358
|
-
/** "thread" (reuse upstream chat) or "temp" (new ephemeral chat per request). */
|
|
359
|
-
/** When true, all requests (personalization, models, media, chat) route exclusively through the browser page (no direct Node fetch). */
|
|
360
|
-
browserOnlyFetch: env.QWEN_BROWSER_ONLY_FETCH !== "false",
|
|
361
|
-
chatMode: env.QWEN_CHAT_MODE,
|
|
362
|
-
maxPromptBytes: Math.max(0, parseInt(env.QWEN_MAX_PROMPT_BYTES)),
|
|
363
|
-
maxPersonalizationBytes: Math.max(
|
|
364
|
-
0,
|
|
365
|
-
parseInt(env.QWEN_MAX_PERSONALIZATION_BYTES),
|
|
366
|
-
),
|
|
367
|
-
deleteAllChatsOnShutdown: env.DELETE_ALL_CHATS_ON_SHUTDOWN === "true",
|
|
368
|
-
/** Send the captured bx-ua/bx-umidtoken headers (real client does NOT). */
|
|
369
|
-
sendBxUa: env.QWEN_SEND_BX_UA === "true",
|
|
370
|
-
/** Deployed web bundle version sent as the `version` API header. */
|
|
371
|
-
webVersion: env.QWEN_WEB_VERSION,
|
|
372
|
-
},
|
|
373
|
-
contextMeter: {
|
|
374
|
-
enabled: env.CONTEXT_METER_ENABLED === "true",
|
|
375
|
-
windowTokens: Math.max(0, parseInt(env.CONTEXT_METER_WINDOW_TOKENS)),
|
|
376
|
-
reportUsage: env.CONTEXT_METER_REPORT_USAGE === "true",
|
|
377
|
-
},
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
export type Config = typeof config;
|
|
381
|
-
|
|
382
|
-
/** Conversation mode: thread-native reuse vs ephemeral temp chat per request. */
|
|
383
|
-
export type ChatMode = "thread" | "temp";
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const envSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
PORT: z
|
|
6
|
+
.string()
|
|
7
|
+
.regex(/^\d+$/, "PORT must be a number")
|
|
8
|
+
.refine((value) => {
|
|
9
|
+
const port = Number(value);
|
|
10
|
+
return port >= 1 && port <= 65535;
|
|
11
|
+
}, "PORT must be between 1 and 65535")
|
|
12
|
+
.default("7936"),
|
|
13
|
+
HOST: z.string().default("0.0.0.0"),
|
|
14
|
+
INTERNAL_HOST: z.string().default("127.0.0.1"),
|
|
15
|
+
USER_AGENT: z
|
|
16
|
+
.string()
|
|
17
|
+
.default(
|
|
18
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
|
|
19
|
+
),
|
|
20
|
+
QWEN_BX_V: z.string().default("2.5.37"),
|
|
21
|
+
// Version header on Qwen API requests = the deployed web bundle version
|
|
22
|
+
// (fallback when dynamic DOM detection is not yet completed).
|
|
23
|
+
// Override via env when Qwen ships a new bundle.
|
|
24
|
+
QWEN_WEB_VERSION: z.string().default("0.2.91"),
|
|
25
|
+
// Controls bx-ua/bx-umidtoken injection on the GENERAL API paths
|
|
26
|
+
// (chats/new, settings): those work without them (live-probed). The
|
|
27
|
+
// completions path is the exception — the 0.2.86 HAR shows the real
|
|
28
|
+
// client POSTs completions WITH bx-ua, so it always includes the captured
|
|
29
|
+
// tokens regardless of this flag (buildCompletionHeaders). Set true
|
|
30
|
+
// to inject them everywhere (legacy behavior).
|
|
31
|
+
QWEN_SEND_BX_UA: z.string().default("false"),
|
|
32
|
+
// Conversation mode: "thread" (default) reuses the upstream Qwen chat via
|
|
33
|
+
// parent_id and sends the thread-native delta; "temp" creates a NEW Qwen
|
|
34
|
+
// temp chat (chat_mode:"local") for every request and sends the full
|
|
35
|
+
// history inline (OpenAI standard). Temp chats are ephemeral and never
|
|
36
|
+
// appear in the account's chat list (live-probed).
|
|
37
|
+
QWEN_CHAT_MODE: z.enum(["thread", "temp"]).default("thread"),
|
|
38
|
+
PLAYWRIGHT_HEADLESS: z.string().default("true"),
|
|
39
|
+
PLAYWRIGHT_BROWSER: z
|
|
40
|
+
.enum(["chromium", "chrome", "edge"])
|
|
41
|
+
.default("chromium"),
|
|
42
|
+
PLAYWRIGHT_INIT_BATCH_SIZE: z.string().default("1"),
|
|
43
|
+
PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS: z.string().default("10000"),
|
|
44
|
+
PLAYWRIGHT_IDLE_CONTEXT_TTL_MS: z.string().default("60000"),
|
|
45
|
+
PLAYWRIGHT_JS_HEAP_MB: z.string().default("256"),
|
|
46
|
+
PLAYWRIGHT_LOW_MEMORY_FLAGS: z.string().default("true"),
|
|
47
|
+
// Keep 2 warm contexts by default ({main + reserve} covers the common
|
|
48
|
+
// failover hop without opening one browser per account): after warmup two
|
|
49
|
+
// browsers stay open; any extra context (simultaneous use / failover) is
|
|
50
|
+
// closed once idle. The cap only evicts IDLE contexts — busy mutexes and
|
|
51
|
+
// active streams are never touched, so concurrent accounts each keep their
|
|
52
|
+
// own context while serving. Accounts in cooldown (rate-limited) sit idle,
|
|
53
|
+
// drop out of the warm set and get evicted.
|
|
54
|
+
PLAYWRIGHT_MAX_ACTIVE_CONTEXTS: z.string().default("2"),
|
|
55
|
+
PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("false"),
|
|
56
|
+
CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
|
|
57
|
+
CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
|
|
58
|
+
CAPTCHA_SOLVER_TIMEOUT_MS: z.string().default("15000"),
|
|
59
|
+
CAPTCHA_SOLVER_RETRY_DELAY_MS: z.string().default("1000"),
|
|
60
|
+
CAPTCHA_SOLVER_SETTLE_MS: z.string().default("2000"),
|
|
61
|
+
CAPTCHA_ACCOUNT_COOLDOWN_MS: z.string().default("120000"),
|
|
62
|
+
// Cap for the escalating quarantine applied when a challenge could NOT be
|
|
63
|
+
// solved (hard block). Each consecutive hard block doubles the window
|
|
64
|
+
// (base = CAPTCHA_ACCOUNT_COOLDOWN_MS) up to this cap.
|
|
65
|
+
CAPTCHA_HARD_BLOCK_MAX_COOLDOWN_MS: z.string().default("3600000"),
|
|
66
|
+
OSS_MULTIPART_THRESHOLD_MB: z.string().default("5"),
|
|
67
|
+
CHAT_REQUEST_LOG: z.string().default("false"),
|
|
68
|
+
HTTP_TIMEOUT: z.string().default("15000"),
|
|
69
|
+
CHAT_TIMEOUT: z.string().default("180000"),
|
|
70
|
+
NAVIGATION_TIMEOUT: z.string().default("60000"),
|
|
71
|
+
PAGE_TIMEOUT: z.string().default("60000"),
|
|
72
|
+
HEADERS_TIMEOUT: z.string().default("90000"),
|
|
73
|
+
TIME_TO_FIRST_BYTE: z.string().default("60000"),
|
|
74
|
+
IDLE_STREAM_TIMEOUT: z.string().default("60000"),
|
|
75
|
+
// Deadline for the FIRST upstream chunk on thinking models (the reasoning
|
|
76
|
+
// idle of 600s is for gaps AFTER data flows; a stream that produced
|
|
77
|
+
// nothing in this window is dead and should fail fast, retryable).
|
|
78
|
+
QWEN_FIRST_CHUNK_TIMEOUT: z.string().default("180000"),
|
|
79
|
+
TOTAL_REQUEST_TIMEOUT: z.string().default("600000"),
|
|
80
|
+
// Mid-stream silence window for thinking models: 3 min with ZERO upstream
|
|
81
|
+
// bytes is a dead stream (WAF swallow / dropped connection) — fail fast and
|
|
82
|
+
// let the retry policy rotate accounts. Flowing reasoning chunks RESET this
|
|
83
|
+
// timer, so legitimate slow thinking is never cut; only total silence is.
|
|
84
|
+
REASONING_MODEL_TIMEOUT: z.string().default("180000"),
|
|
85
|
+
CACHE_TTL: z.string().default("3600"),
|
|
86
|
+
RESPONSE_TTL: z.string().default("1800"),
|
|
87
|
+
CACHE_COMPRESSION_ENABLED: z.string().default("true"),
|
|
88
|
+
CACHE_COMPRESSION_THRESHOLD: z.string().default("1024"),
|
|
89
|
+
CACHE_COMPRESSION_LEVEL: z.string().default("6"),
|
|
90
|
+
METRICS_INTERVAL: z.string().default("10000"),
|
|
91
|
+
WATCHDOG_INTERVAL: z.string().default("5000"),
|
|
92
|
+
WATCHDOG_FAILURES: z.string().default("3"),
|
|
93
|
+
RAM_WARNING: z.string().default("80"),
|
|
94
|
+
RAM_CRITICAL: z.string().default("95"),
|
|
95
|
+
WS_WARNING: z.string().default("50"),
|
|
96
|
+
WS_CRITICAL: z.string().default("100"),
|
|
97
|
+
RETRY_BASE_DELAY_MS: z.string().default(process.env.TEST_MOCK_QWEN_AUTH === "true" ? "50" : "1000"),
|
|
98
|
+
RETRY_MAX_DELAY_MS: z.string().default(process.env.TEST_MOCK_QWEN_AUTH === "true" ? "200" : "10000"),
|
|
99
|
+
RETRY_MAX_ATTEMPTS: z.string().default("3"),
|
|
100
|
+
RETRY_MAX_ACCOUNT_SWITCHES: z.string().default("2"),
|
|
101
|
+
RETRY_ON_UNKNOWN_UPSTREAM: z.string().default("true"),
|
|
102
|
+
RETRY_AUTO_MALFORMED_TOOLS: z.string().default("true"),
|
|
103
|
+
RETRY_AUTO_MALFORMED_TOOLS_MAX: z.string().default("2"),
|
|
104
|
+
MAX_TOOL_CALLS_PER_TURN: z.string().default("24"),
|
|
105
|
+
QWEN_REPEATED_TOOL_CALL_WARN: z.string().default("2"),
|
|
106
|
+
ACCOUNT_MAX_CONCURRENT_STREAMS: z.string().default("2"),
|
|
107
|
+
ACCOUNT_BUSY_WAIT_MS: z.string().default("30000"),
|
|
108
|
+
// Cap for the "wait forever" account-lease queue (thread owner / last
|
|
109
|
+
// usable account): the queue previously had NO deadline, so a stuck lease
|
|
110
|
+
// holder made the next turn wait up to ~600s. A generous finite cap keeps
|
|
111
|
+
// the wait far above the normal 30s but still bounded.
|
|
112
|
+
ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS: z.string().default("120000"),
|
|
113
|
+
// Hard deadline for one stream-acquire attempt. A dead account (upstream
|
|
114
|
+
// swallows the completion fetch) otherwise chains metadata/header timeouts
|
|
115
|
+
// for minutes; this fails the attempt visibly so the retry loop switches.
|
|
116
|
+
ACQUIRE_DEADLINE_MS: z.string().default("120000"),
|
|
117
|
+
ACCOUNT_LEASE_MAX_DURATION_MS: z.string().default("600000"),
|
|
118
|
+
ACCOUNT_INIT_FAILURE_COOLDOWN_MS: z.string().default("300000"),
|
|
119
|
+
// Timeout before a request waiting on the CHAT lock gives up. The chat lock
|
|
120
|
+
// is held for the entire stream lifetime, so it must cover the longest
|
|
121
|
+
// legitimate generation (reasoning models with huge contexts can spend
|
|
122
|
+
// 2-3 min producing the first byte chain). A 60s hard cap turned a normal
|
|
123
|
+
// long turn into a 500 for every concurrent request on the same chat
|
|
124
|
+
// (2026-08-22 production log: lock held 130s -> acquire_deadline 120s).
|
|
125
|
+
CHAT_LOCK_TIMEOUT_MS: z
|
|
126
|
+
.string()
|
|
127
|
+
.regex(/^\d+$/, "CHAT_LOCK_TIMEOUT_MS must be a number")
|
|
128
|
+
.default("180000"),
|
|
129
|
+
STREAM_DISCONNECT_GRACE_MS: z
|
|
130
|
+
.string()
|
|
131
|
+
.regex(/^\d+$/, "STREAM_DISCONNECT_GRACE_MS must be a number")
|
|
132
|
+
.default("4000"),
|
|
133
|
+
CHAT_IN_PROGRESS_RETRY_DELAY_MS: z.string().default("2000"),
|
|
134
|
+
// Temporarily-busy window after a chat_in_progress: long enough to absorb
|
|
135
|
+
// the upstream chat settle (measured ~1-2s), short enough that the next
|
|
136
|
+
// turn of the sticky owner is not pushed to a cold account with a full
|
|
137
|
+
// context replay (8s caused a needless 13.3s hop in the 20:04 session).
|
|
138
|
+
CHAT_IN_PROGRESS_BUSY_MS: z.string().default("4000"),
|
|
139
|
+
// Same-chat retry budget for chat_in_progress. The upstream chat stays "in
|
|
140
|
+
// progress" for 2-16s after a completed turn (grows with turn size); each
|
|
141
|
+
// retry waits a jittered window (busyMs-based) and NO retry re-sends the
|
|
142
|
+
// full context — the escalation (new chat + full replay on a cold account)
|
|
143
|
+
// was the ~1MB re-upload that made tool loops feel like ~40 minutes. After
|
|
144
|
+
// this budget the request FAILS (thread binding kept) and the client's own
|
|
145
|
+
// retry lands on the settled chat with the delta intact.
|
|
146
|
+
CHAT_IN_PROGRESS_MAX_RETRIES: z.string().default("6"),
|
|
147
|
+
MID_STREAM_FAILOVER_THRESHOLD: z.string().default("2"),
|
|
148
|
+
MID_STREAM_FAILOVER_BUSY_MS: z.string().default("60000"),
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
QWEN_BASE_URL: z.string().default("https://chat.qwen.ai"),
|
|
152
|
+
QWEN_CHAT_POOL_SIZE: z.string().default("1"),
|
|
153
|
+
QWEN_CHAT_POOL_MODELS: z.string().default("qwen3.7-plus"),
|
|
154
|
+
QWEN_PERSONALIZATION_FROM_REQUEST: z.string().default("true"),
|
|
155
|
+
QWEN_PERSONALIZATION_VERIFY_GET: z.string().default("true"),
|
|
156
|
+
QWEN_BROWSER_ONLY_FETCH: z.string().default("true"),
|
|
157
|
+
QWEN_MAX_PROMPT_BYTES: z.string().default("0"),
|
|
158
|
+
QWEN_MAX_PERSONALIZATION_BYTES: z.string().default("200000"),
|
|
159
|
+
CONTEXT_METER_ENABLED: z.string().default("true"),
|
|
160
|
+
CONTEXT_METER_WINDOW_TOKENS: z.string().default("0"),
|
|
161
|
+
CONTEXT_METER_REPORT_USAGE: z.string().default("true"),
|
|
162
|
+
DELETE_ALL_CHATS_ON_SHUTDOWN: z.string().default("false"),
|
|
163
|
+
// Keep idle account pages alive (subtlePageActivity + occasional reload).
|
|
164
|
+
// The Baxia WAF scores live page behavior (pointer/scroll events, open
|
|
165
|
+
// session) — an account whose page sits frozen for minutes returns a low
|
|
166
|
+
// trust score and gets TMD-challenged on the next request. On by default;
|
|
167
|
+
// the keeper skips accounts that are mid-stream or mutex-busy.
|
|
168
|
+
SESSION_KEEP_ALIVE_ENABLED: z.string().default("true"),
|
|
169
|
+
SESSION_KEEP_ALIVE_INTERVAL_MS: z.string().default("180000"),
|
|
170
|
+
SESSION_KEEP_ALIVE_IDLE_MS: z.string().default("120000"),
|
|
171
|
+
SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS: z.string().default("480000"),
|
|
172
|
+
API_KEY: z.string().default(""),
|
|
173
|
+
// Static x-ratelimit-* response headers (OpenAI-shaped, doc §5.2). The proxy
|
|
174
|
+
// does not enforce a token/request quota; these exist for SDK/tool parsing.
|
|
175
|
+
RATE_LIMIT_REQUESTS: z.string().default("5000"),
|
|
176
|
+
RATE_LIMIT_TOKENS: z.string().default("200000"),
|
|
177
|
+
})
|
|
178
|
+
;
|
|
179
|
+
|
|
180
|
+
const env = envSchema.parse(process.env);
|
|
181
|
+
|
|
182
|
+
export const config = {
|
|
183
|
+
server: {
|
|
184
|
+
port: parseInt(env.PORT),
|
|
185
|
+
host: env.HOST,
|
|
186
|
+
internalHost: env.INTERNAL_HOST,
|
|
187
|
+
rateLimit: {
|
|
188
|
+
requests: parseInt(env.RATE_LIMIT_REQUESTS),
|
|
189
|
+
tokens: parseInt(env.RATE_LIMIT_TOKENS),
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
logging: {
|
|
193
|
+
chatRequests: env.CHAT_REQUEST_LOG === "true",
|
|
194
|
+
},
|
|
195
|
+
auth: {
|
|
196
|
+
userAgent: env.USER_AGENT,
|
|
197
|
+
bxV: env.QWEN_BX_V,
|
|
198
|
+
},
|
|
199
|
+
playwright: {
|
|
200
|
+
headless: env.PLAYWRIGHT_HEADLESS !== "false",
|
|
201
|
+
browser: env.PLAYWRIGHT_BROWSER,
|
|
202
|
+
initBatchSize: Math.max(1, parseInt(env.PLAYWRIGHT_INIT_BATCH_SIZE)),
|
|
203
|
+
contextCloseTimeoutMs: Math.max(
|
|
204
|
+
1_000,
|
|
205
|
+
parseInt(env.PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS),
|
|
206
|
+
),
|
|
207
|
+
idleContextTtlMs: Math.max(0, parseInt(env.PLAYWRIGHT_IDLE_CONTEXT_TTL_MS)),
|
|
208
|
+
jsHeapMb: Math.max(64, parseInt(env.PLAYWRIGHT_JS_HEAP_MB)),
|
|
209
|
+
lowMemoryFlags: env.PLAYWRIGHT_LOW_MEMORY_FLAGS !== "false",
|
|
210
|
+
maxActiveContexts: Math.max(0, parseInt(env.PLAYWRIGHT_MAX_ACTIVE_CONTEXTS)),
|
|
211
|
+
prepareAllOnStartup: env.PLAYWRIGHT_PREPARE_ALL_ON_STARTUP !== "false",
|
|
212
|
+
},
|
|
213
|
+
captcha: {
|
|
214
|
+
enabled: env.CAPTCHA_SOLVER_ENABLED === "true",
|
|
215
|
+
maxAttempts: Math.max(1, Math.min(5, parseInt(env.CAPTCHA_SOLVER_MAX_ATTEMPTS))),
|
|
216
|
+
timeoutMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_TIMEOUT_MS)),
|
|
217
|
+
retryDelayMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_RETRY_DELAY_MS)),
|
|
218
|
+
settleMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_SETTLE_MS)),
|
|
219
|
+
/** Rest an account whose challenge could not be cleared before reusing it. */
|
|
220
|
+
accountCooldownMs: Math.max(0, parseInt(env.CAPTCHA_ACCOUNT_COOLDOWN_MS)),
|
|
221
|
+
/** Cap for the escalating hard-block quarantine (×2 per consecutive block). */
|
|
222
|
+
hardBlockMaxCooldownMs: Math.max(
|
|
223
|
+
0,
|
|
224
|
+
parseInt(env.CAPTCHA_HARD_BLOCK_MAX_COOLDOWN_MS),
|
|
225
|
+
),
|
|
226
|
+
},
|
|
227
|
+
oss: {
|
|
228
|
+
multipartThresholdBytes: Math.max(
|
|
229
|
+
1 * 1024 * 1024,
|
|
230
|
+
parseInt(env.OSS_MULTIPART_THRESHOLD_MB) * 1024 * 1024,
|
|
231
|
+
),
|
|
232
|
+
},
|
|
233
|
+
timeouts: {
|
|
234
|
+
http: parseInt(env.HTTP_TIMEOUT),
|
|
235
|
+
chat: parseInt(env.CHAT_TIMEOUT),
|
|
236
|
+
navigation: parseInt(env.NAVIGATION_TIMEOUT),
|
|
237
|
+
page: parseInt(env.PAGE_TIMEOUT),
|
|
238
|
+
headers: parseInt(env.HEADERS_TIMEOUT),
|
|
239
|
+
timeToFirstByte: parseInt(env.TIME_TO_FIRST_BYTE),
|
|
240
|
+
idleStreamTimeout: parseInt(env.IDLE_STREAM_TIMEOUT),
|
|
241
|
+
totalRequestTimeout: parseInt(env.TOTAL_REQUEST_TIMEOUT),
|
|
242
|
+
reasoningModelTimeout: parseInt(env.REASONING_MODEL_TIMEOUT),
|
|
243
|
+
firstChunkTimeout: parseInt(env.QWEN_FIRST_CHUNK_TIMEOUT),
|
|
244
|
+
},
|
|
245
|
+
cache: {
|
|
246
|
+
defaultTTL: parseInt(env.CACHE_TTL),
|
|
247
|
+
responseTTL: parseInt(env.RESPONSE_TTL),
|
|
248
|
+
compression: {
|
|
249
|
+
enabled: env.CACHE_COMPRESSION_ENABLED !== "false",
|
|
250
|
+
threshold: parseInt(env.CACHE_COMPRESSION_THRESHOLD),
|
|
251
|
+
level: parseInt(env.CACHE_COMPRESSION_LEVEL),
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
metrics: {
|
|
256
|
+
interval: parseInt(env.METRICS_INTERVAL),
|
|
257
|
+
},
|
|
258
|
+
watchdog: {
|
|
259
|
+
checkInterval: parseInt(env.WATCHDOG_INTERVAL),
|
|
260
|
+
consecutiveFailuresThreshold: parseInt(env.WATCHDOG_FAILURES),
|
|
261
|
+
ram: {
|
|
262
|
+
warningThreshold: parseInt(env.RAM_WARNING),
|
|
263
|
+
criticalThreshold: parseInt(env.RAM_CRITICAL),
|
|
264
|
+
},
|
|
265
|
+
streams: {
|
|
266
|
+
warningThreshold: parseInt(env.WS_WARNING),
|
|
267
|
+
criticalThreshold: parseInt(env.WS_CRITICAL),
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
retry: {
|
|
271
|
+
baseDelayMs: parseInt(env.RETRY_BASE_DELAY_MS),
|
|
272
|
+
maxDelayMs: parseInt(env.RETRY_MAX_DELAY_MS),
|
|
273
|
+
maxAttempts: Math.max(1, parseInt(env.RETRY_MAX_ATTEMPTS)),
|
|
274
|
+
maxAccountSwitches: Math.max(0, parseInt(env.RETRY_MAX_ACCOUNT_SWITCHES)),
|
|
275
|
+
onUnknownUpstream: env.RETRY_ON_UNKNOWN_UPSTREAM !== "false",
|
|
276
|
+
chatInProgressDelayMs: Math.max(0, parseInt(env.CHAT_IN_PROGRESS_RETRY_DELAY_MS)),
|
|
277
|
+
chatInProgressBusyMs: Math.max(0, parseInt(env.CHAT_IN_PROGRESS_BUSY_MS)),
|
|
278
|
+
chatInProgressMaxAttempts: Math.max(1, parseInt(env.CHAT_IN_PROGRESS_MAX_RETRIES)),
|
|
279
|
+
midStreamFailoverThreshold: Math.max(
|
|
280
|
+
0,
|
|
281
|
+
parseInt(env.MID_STREAM_FAILOVER_THRESHOLD),
|
|
282
|
+
),
|
|
283
|
+
midStreamFailoverBusyMs: Math.max(
|
|
284
|
+
0,
|
|
285
|
+
parseInt(env.MID_STREAM_FAILOVER_BUSY_MS),
|
|
286
|
+
),
|
|
287
|
+
autoRetryMalformedTools: env.RETRY_AUTO_MALFORMED_TOOLS !== "false",
|
|
288
|
+
autoRetryMalformedToolsMax: Math.max(1, parseInt(env.RETRY_AUTO_MALFORMED_TOOLS_MAX)),
|
|
289
|
+
maxToolCallsPerTurn: Math.max(0, parseInt(env.MAX_TOOL_CALLS_PER_TURN)),
|
|
290
|
+
repeatedToolCallWarnThreshold: Math.max(
|
|
291
|
+
1,
|
|
292
|
+
parseInt(env.QWEN_REPEATED_TOOL_CALL_WARN),
|
|
293
|
+
),
|
|
294
|
+
},
|
|
295
|
+
concurrency: {
|
|
296
|
+
maxStreamsPerAccount: Math.max(1, parseInt(env.ACCOUNT_MAX_CONCURRENT_STREAMS)),
|
|
297
|
+
busyWaitMs: Math.max(
|
|
298
|
+
0,
|
|
299
|
+
Number.isFinite(parseInt(env.ACCOUNT_BUSY_WAIT_MS as string))
|
|
300
|
+
? parseInt(env.ACCOUNT_BUSY_WAIT_MS as string)
|
|
301
|
+
: 30_000,
|
|
302
|
+
),
|
|
303
|
+
/** Bound for the "wait forever" lease queue (default 2 min). */
|
|
304
|
+
queueWaitForeverCapMs: Math.max(
|
|
305
|
+
0,
|
|
306
|
+
Number.isFinite(parseInt(env.ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS as string))
|
|
307
|
+
? parseInt(env.ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS as string)
|
|
308
|
+
: 120_000,
|
|
309
|
+
),
|
|
310
|
+
/** Hard deadline for one stream-acquire attempt (default 2 min). */
|
|
311
|
+
acquireDeadlineMs: Math.max(
|
|
312
|
+
0,
|
|
313
|
+
Number.isFinite(parseInt(env.ACQUIRE_DEADLINE_MS as string))
|
|
314
|
+
? parseInt(env.ACQUIRE_DEADLINE_MS as string)
|
|
315
|
+
: 120_000,
|
|
316
|
+
),
|
|
317
|
+
/** Safety net: force-release leases held longer than this (default 10 min). */
|
|
318
|
+
leaseMaxDurationMs: Math.max(
|
|
319
|
+
0,
|
|
320
|
+
parseInt(env.ACCOUNT_LEASE_MAX_DURATION_MS),
|
|
321
|
+
),
|
|
322
|
+
initFailureCooldownMs: Math.max(
|
|
323
|
+
30_000,
|
|
324
|
+
parseInt(env.ACCOUNT_INIT_FAILURE_COOLDOWN_MS),
|
|
325
|
+
),
|
|
326
|
+
/** Max time a request waits on the per-chat lock (default 3 min). */
|
|
327
|
+
chatLockTimeoutMs: Math.max(
|
|
328
|
+
0,
|
|
329
|
+
parseInt(env.CHAT_LOCK_TIMEOUT_MS),
|
|
330
|
+
),
|
|
331
|
+
},
|
|
332
|
+
stream: {
|
|
333
|
+
disconnectGraceMs: Math.max(
|
|
334
|
+
0,
|
|
335
|
+
parseInt(env.STREAM_DISCONNECT_GRACE_MS),
|
|
336
|
+
),
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
sessionKeeper: {
|
|
341
|
+
enabled: env.SESSION_KEEP_ALIVE_ENABLED !== "false",
|
|
342
|
+
intervalMs: parseInt(env.SESSION_KEEP_ALIVE_INTERVAL_MS),
|
|
343
|
+
idleMs: parseInt(env.SESSION_KEEP_ALIVE_IDLE_MS),
|
|
344
|
+
navigationIntervalMs: parseInt(
|
|
345
|
+
env.SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS,
|
|
346
|
+
),
|
|
347
|
+
},
|
|
348
|
+
apiKey: env.API_KEY,
|
|
349
|
+
qwen: {
|
|
350
|
+
baseUrl: env.QWEN_BASE_URL,
|
|
351
|
+
chatPoolSize: Math.max(0, parseInt(env.QWEN_CHAT_POOL_SIZE)),
|
|
352
|
+
chatPoolModels: env.QWEN_CHAT_POOL_MODELS.split(",")
|
|
353
|
+
.map((model) => model.trim())
|
|
354
|
+
.filter(Boolean),
|
|
355
|
+
personalizationFromRequest:
|
|
356
|
+
env.QWEN_PERSONALIZATION_FROM_REQUEST === "true",
|
|
357
|
+
personalizationVerifyGet: env.QWEN_PERSONALIZATION_VERIFY_GET !== "false",
|
|
358
|
+
/** "thread" (reuse upstream chat) or "temp" (new ephemeral chat per request). */
|
|
359
|
+
/** When true, all requests (personalization, models, media, chat) route exclusively through the browser page (no direct Node fetch). */
|
|
360
|
+
browserOnlyFetch: env.QWEN_BROWSER_ONLY_FETCH !== "false",
|
|
361
|
+
chatMode: env.QWEN_CHAT_MODE,
|
|
362
|
+
maxPromptBytes: Math.max(0, parseInt(env.QWEN_MAX_PROMPT_BYTES)),
|
|
363
|
+
maxPersonalizationBytes: Math.max(
|
|
364
|
+
0,
|
|
365
|
+
parseInt(env.QWEN_MAX_PERSONALIZATION_BYTES),
|
|
366
|
+
),
|
|
367
|
+
deleteAllChatsOnShutdown: env.DELETE_ALL_CHATS_ON_SHUTDOWN === "true",
|
|
368
|
+
/** Send the captured bx-ua/bx-umidtoken headers (real client does NOT). */
|
|
369
|
+
sendBxUa: env.QWEN_SEND_BX_UA === "true",
|
|
370
|
+
/** Deployed web bundle version sent as the `version` API header. */
|
|
371
|
+
webVersion: env.QWEN_WEB_VERSION,
|
|
372
|
+
},
|
|
373
|
+
contextMeter: {
|
|
374
|
+
enabled: env.CONTEXT_METER_ENABLED === "true",
|
|
375
|
+
windowTokens: Math.max(0, parseInt(env.CONTEXT_METER_WINDOW_TOKENS)),
|
|
376
|
+
reportUsage: env.CONTEXT_METER_REPORT_USAGE === "true",
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
export type Config = typeof config;
|
|
381
|
+
|
|
382
|
+
/** Conversation mode: thread-native reuse vs ephemeral temp chat per request. */
|
|
383
|
+
export type ChatMode = "thread" | "temp";
|