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
|
@@ -1,297 +1,301 @@
|
|
|
1
|
-
import {
|
|
2
|
-
QwenAccount,
|
|
3
|
-
loadAccounts,
|
|
4
|
-
updateAccountCooldown,
|
|
5
|
-
} from "./accounts.ts";
|
|
6
|
-
import { getAccountsByPriority } from "./account-priority.ts";
|
|
7
|
-
import { formatCooldownUntil } from "./logger.ts";
|
|
8
|
-
|
|
9
|
-
let currentIndex = 0;
|
|
10
|
-
|
|
11
|
-
interface CooldownEntry {
|
|
12
|
-
until: number;
|
|
13
|
-
reason: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const cooldowns = new Map<string, CooldownEntry>();
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Milliseconds until the next UTC midnight plus a safety margin. The Qwen
|
|
20
|
-
* daily quota resets at 00:00 UTC, so this is the correct "when is this
|
|
21
|
-
* account usable again" for a quota exhaust — regardless of the upstream
|
|
22
|
-
* "Wait about N hour(s)" hint (accurate mid-day, but rounds to ~24h near
|
|
23
|
-
* midnight when the real reset is minutes away).
|
|
24
|
-
*/
|
|
25
|
-
export function computeQuotaCooldownMs(
|
|
26
|
-
nowMs: number,
|
|
27
|
-
marginMs = 5 * 60 * 1000,
|
|
28
|
-
): number {
|
|
29
|
-
const nextMidnight = new Date(nowMs);
|
|
30
|
-
nextMidnight.setUTCHours(24, 0, 0, 0);
|
|
31
|
-
const targetMs = nextMidnight.getTime() - nowMs + marginMs;
|
|
32
|
-
// A daily quota cooldown must never exceed 24h (cap to 24h - 1m so it never spills over
|
|
33
|
-
// during the first marginMs window after 00:00 UTC).
|
|
34
|
-
const maxCooldownMs = 24 * 60 * 60 * 1000 - 60_000;
|
|
35
|
-
return Math.min(maxCooldownMs, Math.max(60_000, targetMs));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// The long-ago 24h blind fallback was the source of "treated available accounts
|
|
39
|
-
// as unavailable": when no explicit duration was given (e.g. a quota exhaust
|
|
40
|
-
// without the wait hint) the account was parked for a full day even though the
|
|
41
|
-
// Qwen daily quota resets at the next UTC midnight. Fall back to the same
|
|
42
|
-
// midnight-based behavior instead.
|
|
43
|
-
function defaultCooldownDurationMs(): number {
|
|
44
|
-
return computeQuotaCooldownMs(Date.now());
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function markAccountRateLimited(
|
|
48
|
-
accountId: string,
|
|
49
|
-
cooldownMs?: number,
|
|
50
|
-
reason?: string,
|
|
51
|
-
options: { silent?: boolean } = {},
|
|
52
|
-
): void {
|
|
53
|
-
const duration = cooldownMs ?? defaultCooldownDurationMs();
|
|
54
|
-
const until = Date.now() + duration;
|
|
55
|
-
const cooldownReason = reason ?? "RateLimited";
|
|
56
|
-
|
|
57
|
-
cooldowns.set(accountId, {
|
|
58
|
-
until,
|
|
59
|
-
reason: cooldownReason,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Persist to database
|
|
63
|
-
if (accountId !== "global") {
|
|
64
|
-
try {
|
|
65
|
-
updateAccountCooldown(accountId, until, cooldownReason);
|
|
66
|
-
} catch (err) {
|
|
67
|
-
console.error(
|
|
68
|
-
`❌ [AccountManager] Failed to save cooldown to DB for ${accountId}:`,
|
|
69
|
-
(err as Error).message,
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (!options.silent) {
|
|
75
|
-
console.log(
|
|
76
|
-
`⏱️ [AccountManager] Cooldown set | ${accountId} | reason=${cooldownReason} | ${Math.round(duration / 1000)}s | until=${formatCooldownUntil(new Date(until))}`,
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function clearAccountCooldown(accountId: string): void {
|
|
82
|
-
cooldowns.delete(accountId);
|
|
83
|
-
if (accountId !== "global") {
|
|
84
|
-
try {
|
|
85
|
-
updateAccountCooldown(accountId, 0, null);
|
|
86
|
-
} catch (err) {
|
|
87
|
-
console.error(
|
|
88
|
-
`❌ [AccountManager] Failed to clear cooldown in DB for ${accountId}:`,
|
|
89
|
-
(err as Error).message,
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function clearAllAccountCooldowns(): number {
|
|
96
|
-
const accounts = loadAccounts();
|
|
97
|
-
let count = 0;
|
|
98
|
-
for (const account of accounts) {
|
|
99
|
-
if (cooldowns.has(account.id) || (account.cooldown_until && account.cooldown_until > 0)) {
|
|
100
|
-
clearAccountCooldown(account.id);
|
|
101
|
-
count++;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
cooldowns.delete("global");
|
|
105
|
-
return count;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function getAccountCooldownInfo(
|
|
109
|
-
accountId: string,
|
|
110
|
-
): { onCooldown: boolean; remainingMs: number; reason: string } | null {
|
|
111
|
-
const entry = cooldowns.get(accountId);
|
|
112
|
-
if (!entry) return null;
|
|
113
|
-
const remaining = entry.until - Date.now();
|
|
114
|
-
if (remaining <= 0) {
|
|
115
|
-
cooldowns.delete(accountId);
|
|
116
|
-
if (accountId !== "global") {
|
|
117
|
-
try {
|
|
118
|
-
updateAccountCooldown(accountId, 0, null);
|
|
119
|
-
} catch (err) {
|
|
120
|
-
console.error(
|
|
121
|
-
`❌ [AccountManager] Failed to clear expired cooldown in DB:`,
|
|
122
|
-
(err as Error).message,
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
return { onCooldown: true, remainingMs: remaining, reason: entry.reason };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function isAccountOnCooldown(accountId: string): boolean {
|
|
132
|
-
return getAccountCooldownInfo(accountId) !== null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ─── Headers-ready gate (mirrors upstream `markAccountReady`) ───────────────
|
|
136
|
-
// Accounts whose anti-bot headers were successfully captured are "ready". The
|
|
137
|
-
// rotation pickers below skip not-ready accounts whenever at least one account
|
|
138
|
-
// IS ready, so a request never lands on a lane that is still warming up or
|
|
139
|
-
// whose context just died (Playwright page unavailable → 300s init cooldown).
|
|
140
|
-
// The gate degrades to "all accounts pass" when NO account is ready (startup
|
|
141
|
-
// warmup / freshly-restored headers) so a single-account or cold pool stays
|
|
142
|
-
// lossless — exactly the upstream `anyReady` rule.
|
|
143
|
-
const headersReadyAccounts = new Set<string>();
|
|
144
|
-
|
|
145
|
-
export function markAccountHeadersReady(accountId: string): void {
|
|
146
|
-
if (!accountId || accountId === "global") return;
|
|
147
|
-
headersReadyAccounts.add(accountId);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function unmarkAccountHeadersReady(accountId: string): void {
|
|
151
|
-
if (!accountId) return;
|
|
152
|
-
headersReadyAccounts.delete(accountId);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function isAccountHeadersReady(accountId: string): boolean {
|
|
156
|
-
return headersReadyAccounts.has(accountId);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
//
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
QwenAccount,
|
|
3
|
+
loadAccounts,
|
|
4
|
+
updateAccountCooldown,
|
|
5
|
+
} from "./accounts.ts";
|
|
6
|
+
import { getAccountsByPriority } from "./account-priority.ts";
|
|
7
|
+
import { formatCooldownUntil } from "./logger.ts";
|
|
8
|
+
|
|
9
|
+
let currentIndex = 0;
|
|
10
|
+
|
|
11
|
+
interface CooldownEntry {
|
|
12
|
+
until: number;
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const cooldowns = new Map<string, CooldownEntry>();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Milliseconds until the next UTC midnight plus a safety margin. The Qwen
|
|
20
|
+
* daily quota resets at 00:00 UTC, so this is the correct "when is this
|
|
21
|
+
* account usable again" for a quota exhaust — regardless of the upstream
|
|
22
|
+
* "Wait about N hour(s)" hint (accurate mid-day, but rounds to ~24h near
|
|
23
|
+
* midnight when the real reset is minutes away).
|
|
24
|
+
*/
|
|
25
|
+
export function computeQuotaCooldownMs(
|
|
26
|
+
nowMs: number,
|
|
27
|
+
marginMs = 5 * 60 * 1000,
|
|
28
|
+
): number {
|
|
29
|
+
const nextMidnight = new Date(nowMs);
|
|
30
|
+
nextMidnight.setUTCHours(24, 0, 0, 0);
|
|
31
|
+
const targetMs = nextMidnight.getTime() - nowMs + marginMs;
|
|
32
|
+
// A daily quota cooldown must never exceed 24h (cap to 24h - 1m so it never spills over
|
|
33
|
+
// during the first marginMs window after 00:00 UTC).
|
|
34
|
+
const maxCooldownMs = 24 * 60 * 60 * 1000 - 60_000;
|
|
35
|
+
return Math.min(maxCooldownMs, Math.max(60_000, targetMs));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The long-ago 24h blind fallback was the source of "treated available accounts
|
|
39
|
+
// as unavailable": when no explicit duration was given (e.g. a quota exhaust
|
|
40
|
+
// without the wait hint) the account was parked for a full day even though the
|
|
41
|
+
// Qwen daily quota resets at the next UTC midnight. Fall back to the same
|
|
42
|
+
// midnight-based behavior instead.
|
|
43
|
+
function defaultCooldownDurationMs(): number {
|
|
44
|
+
return computeQuotaCooldownMs(Date.now());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function markAccountRateLimited(
|
|
48
|
+
accountId: string,
|
|
49
|
+
cooldownMs?: number,
|
|
50
|
+
reason?: string,
|
|
51
|
+
options: { silent?: boolean } = {},
|
|
52
|
+
): void {
|
|
53
|
+
const duration = cooldownMs ?? defaultCooldownDurationMs();
|
|
54
|
+
const until = Date.now() + duration;
|
|
55
|
+
const cooldownReason = reason ?? "RateLimited";
|
|
56
|
+
|
|
57
|
+
cooldowns.set(accountId, {
|
|
58
|
+
until,
|
|
59
|
+
reason: cooldownReason,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Persist to database
|
|
63
|
+
if (accountId !== "global") {
|
|
64
|
+
try {
|
|
65
|
+
updateAccountCooldown(accountId, until, cooldownReason);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error(
|
|
68
|
+
`❌ [AccountManager] Failed to save cooldown to DB for ${accountId}:`,
|
|
69
|
+
(err as Error).message,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!options.silent) {
|
|
75
|
+
console.log(
|
|
76
|
+
`⏱️ [AccountManager] Cooldown set | ${accountId} | reason=${cooldownReason} | ${Math.round(duration / 1000)}s | until=${formatCooldownUntil(new Date(until))}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function clearAccountCooldown(accountId: string): void {
|
|
82
|
+
cooldowns.delete(accountId);
|
|
83
|
+
if (accountId !== "global") {
|
|
84
|
+
try {
|
|
85
|
+
updateAccountCooldown(accountId, 0, null);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error(
|
|
88
|
+
`❌ [AccountManager] Failed to clear cooldown in DB for ${accountId}:`,
|
|
89
|
+
(err as Error).message,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function clearAllAccountCooldowns(): number {
|
|
96
|
+
const accounts = loadAccounts();
|
|
97
|
+
let count = 0;
|
|
98
|
+
for (const account of accounts) {
|
|
99
|
+
if (cooldowns.has(account.id) || (account.cooldown_until && account.cooldown_until > 0)) {
|
|
100
|
+
clearAccountCooldown(account.id);
|
|
101
|
+
count++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
cooldowns.delete("global");
|
|
105
|
+
return count;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getAccountCooldownInfo(
|
|
109
|
+
accountId: string,
|
|
110
|
+
): { onCooldown: boolean; remainingMs: number; reason: string } | null {
|
|
111
|
+
const entry = cooldowns.get(accountId);
|
|
112
|
+
if (!entry) return null;
|
|
113
|
+
const remaining = entry.until - Date.now();
|
|
114
|
+
if (remaining <= 0) {
|
|
115
|
+
cooldowns.delete(accountId);
|
|
116
|
+
if (accountId !== "global") {
|
|
117
|
+
try {
|
|
118
|
+
updateAccountCooldown(accountId, 0, null);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error(
|
|
121
|
+
`❌ [AccountManager] Failed to clear expired cooldown in DB:`,
|
|
122
|
+
(err as Error).message,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return { onCooldown: true, remainingMs: remaining, reason: entry.reason };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isAccountOnCooldown(accountId: string): boolean {
|
|
132
|
+
return getAccountCooldownInfo(accountId) !== null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Headers-ready gate (mirrors upstream `markAccountReady`) ───────────────
|
|
136
|
+
// Accounts whose anti-bot headers were successfully captured are "ready". The
|
|
137
|
+
// rotation pickers below skip not-ready accounts whenever at least one account
|
|
138
|
+
// IS ready, so a request never lands on a lane that is still warming up or
|
|
139
|
+
// whose context just died (Playwright page unavailable → 300s init cooldown).
|
|
140
|
+
// The gate degrades to "all accounts pass" when NO account is ready (startup
|
|
141
|
+
// warmup / freshly-restored headers) so a single-account or cold pool stays
|
|
142
|
+
// lossless — exactly the upstream `anyReady` rule.
|
|
143
|
+
const headersReadyAccounts = new Set<string>();
|
|
144
|
+
|
|
145
|
+
export function markAccountHeadersReady(accountId: string): void {
|
|
146
|
+
if (!accountId || accountId === "global") return;
|
|
147
|
+
headersReadyAccounts.add(accountId);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function unmarkAccountHeadersReady(accountId: string): void {
|
|
151
|
+
if (!accountId) return;
|
|
152
|
+
headersReadyAccounts.delete(accountId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function isAccountHeadersReady(accountId: string): boolean {
|
|
156
|
+
return headersReadyAccounts.has(accountId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getHeadersReadyAccountIds(): string[] {
|
|
160
|
+
return Array.from(headersReadyAccounts);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function anyUsableAccountHeadersReady(
|
|
164
|
+
accounts: QwenAccount[],
|
|
165
|
+
triedSet?: Set<string>,
|
|
166
|
+
): boolean {
|
|
167
|
+
return accounts.some(
|
|
168
|
+
(a) =>
|
|
169
|
+
(!triedSet || !triedSet.has(a.id)) &&
|
|
170
|
+
!isAccountOnCooldown(a.id) &&
|
|
171
|
+
isAccountHeadersReady(a.id),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function passesHeadersReadyGate(
|
|
176
|
+
accountId: string,
|
|
177
|
+
anyReady: boolean,
|
|
178
|
+
): boolean {
|
|
179
|
+
return !anyReady || isAccountHeadersReady(accountId);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function syncCooldownsFromDb(accounts: QwenAccount[]): void {
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
for (const account of accounts) {
|
|
185
|
+
if (account.cooldown_until && account.cooldown_until > now) {
|
|
186
|
+
if (!cooldowns.has(account.id)) {
|
|
187
|
+
cooldowns.set(account.id, {
|
|
188
|
+
until: account.cooldown_until,
|
|
189
|
+
reason: account.cooldown_reason || "RateLimited",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
if (cooldowns.has(account.id)) {
|
|
194
|
+
cooldowns.delete(account.id);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getNextAccount(): QwenAccount | null {
|
|
201
|
+
const accounts = loadAccounts();
|
|
202
|
+
if (accounts.length === 0) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
syncCooldownsFromDb(accounts);
|
|
207
|
+
|
|
208
|
+
// Ordena por prioridade (contas que funcionaram bem vêm primeiro)
|
|
209
|
+
const prioritized = getAccountsByPriority(accounts);
|
|
210
|
+
// Gate: once ANY usable account has captured headers, only ready accounts rotate.
|
|
211
|
+
// If all ready accounts are on cooldown, anyReady degrades to false so non-ready
|
|
212
|
+
// accounts can be initialized on-demand instead of falsely reporting pool exhaustion.
|
|
213
|
+
const anyReady = anyUsableAccountHeadersReady(accounts);
|
|
214
|
+
|
|
215
|
+
for (let i = 0; i < prioritized.length; i++) {
|
|
216
|
+
const account = prioritized[currentIndex % prioritized.length];
|
|
217
|
+
currentIndex = (currentIndex + 1) % prioritized.length;
|
|
218
|
+
if (
|
|
219
|
+
!isAccountOnCooldown(account.id) &&
|
|
220
|
+
passesHeadersReadyGate(account.id, anyReady)
|
|
221
|
+
) {
|
|
222
|
+
return account;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// All accounts on cooldown — return the one with the shortest remaining cooldown.
|
|
227
|
+
let best: QwenAccount | null = null;
|
|
228
|
+
let bestRemaining = Infinity;
|
|
229
|
+
for (const account of prioritized) {
|
|
230
|
+
const info = getAccountCooldownInfo(account.id);
|
|
231
|
+
if (info && info.remainingMs < bestRemaining) {
|
|
232
|
+
bestRemaining = info.remainingMs;
|
|
233
|
+
best = account;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return best;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function getNextAvailableAccount(
|
|
240
|
+
triedAccountIds?: Set<string> | string,
|
|
241
|
+
): QwenAccount | null {
|
|
242
|
+
const accounts = loadAccounts();
|
|
243
|
+
if (accounts.length === 0) return null;
|
|
244
|
+
|
|
245
|
+
syncCooldownsFromDb(accounts);
|
|
246
|
+
|
|
247
|
+
let triedSet: Set<string>;
|
|
248
|
+
if (triedAccountIds instanceof Set) {
|
|
249
|
+
triedSet = triedAccountIds;
|
|
250
|
+
} else {
|
|
251
|
+
triedSet = new Set(triedAccountIds ? [triedAccountIds] : []);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Ordena por prioridade (contas que funcionaram bem vêm primeiro)
|
|
255
|
+
const prioritized = getAccountsByPriority(accounts);
|
|
256
|
+
// Gate: once ANY untried, non-cooldown account has captured headers, only ready accounts rotate.
|
|
257
|
+
// If all ready accounts are on cooldown or tried, anyReady degrades to false so non-ready
|
|
258
|
+
// accounts can be initialized on-demand instead of falsely reporting pool exhaustion.
|
|
259
|
+
const anyReady = anyUsableAccountHeadersReady(accounts, triedSet);
|
|
260
|
+
|
|
261
|
+
// 1. Try to find an untried account that is NOT on cooldown
|
|
262
|
+
for (let i = 0; i < prioritized.length; i++) {
|
|
263
|
+
const idx = (currentIndex + i) % prioritized.length;
|
|
264
|
+
const account = prioritized[idx];
|
|
265
|
+
if (triedSet.has(account.id)) continue;
|
|
266
|
+
if (
|
|
267
|
+
!isAccountOnCooldown(account.id) &&
|
|
268
|
+
passesHeadersReadyGate(account.id, anyReady)
|
|
269
|
+
) {
|
|
270
|
+
currentIndex = (idx + 1) % prioritized.length;
|
|
271
|
+
return account;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 2. If all untried accounts are on cooldown, return the untried one with the shortest remaining cooldown
|
|
276
|
+
let best: QwenAccount | null = null;
|
|
277
|
+
let bestRemaining = Infinity;
|
|
278
|
+
for (const account of prioritized) {
|
|
279
|
+
if (triedSet.has(account.id)) continue;
|
|
280
|
+
const info = getAccountCooldownInfo(account.id);
|
|
281
|
+
if (info && info.remainingMs < bestRemaining) {
|
|
282
|
+
bestRemaining = info.remainingMs;
|
|
283
|
+
best = account;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return best;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function getCooldownStatus(): Record<
|
|
290
|
+
string,
|
|
291
|
+
{ remainingMs: number; reason: string }
|
|
292
|
+
> {
|
|
293
|
+
const result: Record<string, { remainingMs: number; reason: string }> = {};
|
|
294
|
+
for (const [id, info] of cooldowns.entries()) {
|
|
295
|
+
const remaining = info.until - Date.now();
|
|
296
|
+
if (remaining > 0) {
|
|
297
|
+
result[id] = { remainingMs: remaining, reason: info.reason };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|