qwenproxy-cli 1.6.4 → 1.6.6
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/dist/clean-cache.js +84 -11
- package/dist/delete-chats.js +183 -45
- package/dist/index.js +267 -77
- package/dist/login.js +92 -16
- package/dist/reset-cooldowns.js +87 -12
- package/package.json +1 -1
- package/src/core/mutex.ts +4 -0
- package/src/core/version.ts +1 -1
- package/src/routes/anthropic/translate.ts +15 -7
- package/src/routes/chat/helpers.ts +4 -2
- package/src/routes/chat/streaming.ts +66 -31
- package/src/routes/responses/adapter.ts +4 -1
- package/src/services/context-meter.ts +1 -1
- package/src/services/playwright.ts +57 -20
- package/src/services/qwen-headers.ts +3 -2
- package/src/services/qwen.ts +87 -30
- package/src/tools/tool-call-heuristics.ts +88 -0
- package/src/utils/timezone.ts +56 -0
|
@@ -33,6 +33,10 @@ import {
|
|
|
33
33
|
import type { Message, OpenAIRequest, Usage } from "../../utils/types.ts";
|
|
34
34
|
import { touchChatLock } from "./account.ts";
|
|
35
35
|
import { StreamingToolParser } from "../../tools/parser.ts";
|
|
36
|
+
import {
|
|
37
|
+
decideMissingToolCallRetry,
|
|
38
|
+
type MissingToolCallReason,
|
|
39
|
+
} from "../../tools/tool-call-heuristics.ts";
|
|
36
40
|
import {
|
|
37
41
|
getStream,
|
|
38
42
|
markStreamEmitted,
|
|
@@ -609,8 +613,17 @@ export async function processNonStreamingResponse(
|
|
|
609
613
|
|
|
610
614
|
const finishReason = toolCallsOut.length ? "tool_calls" : "stop";
|
|
611
615
|
|
|
612
|
-
// Auto-retry if all tool calls were malformed
|
|
613
|
-
const
|
|
616
|
+
// Auto-retry if all tool calls were malformed or model hesitated with missing call
|
|
617
|
+
const retryDecision = toolParser
|
|
618
|
+
? decideMissingToolCallRetry({
|
|
619
|
+
declaredToolCount: declaredTools.length,
|
|
620
|
+
successfulToolCallCount: toolParser.getSuccessfulToolCallCount(),
|
|
621
|
+
malformedToolCallCount: toolParser.getMalformedToolCalls().length,
|
|
622
|
+
emittedToolCallCount: toolParser.getEmittedToolCallCount(),
|
|
623
|
+
text: finalContent,
|
|
624
|
+
})
|
|
625
|
+
: { retry: false, reason: null as MissingToolCallReason };
|
|
626
|
+
const allToolsFailed = retryDecision.retry;
|
|
614
627
|
const malformedRetryCount = midStreamRetry?.malformedRetryCount ?? 0;
|
|
615
628
|
if (
|
|
616
629
|
allToolsFailed &&
|
|
@@ -618,7 +631,7 @@ export async function processNonStreamingResponse(
|
|
|
618
631
|
midStreamRetry &&
|
|
619
632
|
malformedRetryCount < config.retry.autoRetryMalformedToolsMax
|
|
620
633
|
) {
|
|
621
|
-
const malformedCalls = toolParser
|
|
634
|
+
const malformedCalls = toolParser?.getMalformedToolCalls() ?? [];
|
|
622
635
|
const malformedCount = malformedCalls.length;
|
|
623
636
|
|
|
624
637
|
// Build detailed error message with available tools list
|
|
@@ -634,7 +647,9 @@ export async function processNonStreamingResponse(
|
|
|
634
647
|
: "";
|
|
635
648
|
|
|
636
649
|
let errorMessage: string;
|
|
637
|
-
if (
|
|
650
|
+
if (retryDecision.reason === "no_call") {
|
|
651
|
+
errorMessage = `You did not emit any tool call. Respond with a single tool-call block using one of the declared tools.${toolsHint}`;
|
|
652
|
+
} else if (undeclaredNames.length > 0) {
|
|
638
653
|
errorMessage = `Your previous ${malformedCount} tool call(s) used undeclared tool names: ${undeclaredNames.join(", ")}. Only declared tools can be executed. Please retry with valid tool names.${toolsHint}`;
|
|
639
654
|
} else {
|
|
640
655
|
const previews = malformedCalls
|
|
@@ -719,7 +734,7 @@ export async function processNonStreamingResponse(
|
|
|
719
734
|
);
|
|
720
735
|
}
|
|
721
736
|
|
|
722
|
-
console.log(`🔄 [Chat] Auto-retry (${malformedRetryCount + 1}/${config.retry.autoRetryMalformedToolsMax}) | ${newStreamResult.activeAccountLabel} | ${body.model} | chat=${newStreamResult.uiSessionId.substring(0, 12)} | reason
|
|
737
|
+
console.log(`🔄 [Chat] Auto-retry (${malformedRetryCount + 1}/${config.retry.autoRetryMalformedToolsMax}) | ${newStreamResult.activeAccountLabel} | ${body.model} | chat=${newStreamResult.uiSessionId.substring(0, 12)} | reason=${retryDecision.reason === "no_call" ? "missing_tool_call" : "malformed_tool_calls"}`);
|
|
723
738
|
|
|
724
739
|
// Process the new stream. Propagate the retry context with an incremented
|
|
725
740
|
// counter so a subsequent malformed response can retry again up to
|
|
@@ -922,7 +937,7 @@ export async function processStreamingResponse(
|
|
|
922
937
|
let chatInProgressSameAccountRetries = 0;
|
|
923
938
|
let antiBotSameAccountRetries = 0;
|
|
924
939
|
const totalConfiguredAccounts = loadAccounts().length;
|
|
925
|
-
let quotaRotationsLeft = Math.
|
|
940
|
+
let quotaRotationsLeft = Math.max(1, totalConfiguredAccounts - 1);
|
|
926
941
|
// Set when the terminal [DONE] reached the client. The `| recovered`
|
|
927
942
|
// suffix on Stream done must only appear for attempts that COMPLETED after
|
|
928
943
|
// a mid-stream retry, not for failed attempts that consumed retries.
|
|
@@ -2164,9 +2179,14 @@ export async function processStreamingResponse(
|
|
|
2164
2179
|
|
|
2165
2180
|
while (malformedRetryCount < maxMalformedRetries) {
|
|
2166
2181
|
if (!toolParser) break;
|
|
2167
|
-
const
|
|
2168
|
-
|
|
2169
|
-
toolParser.getSuccessfulToolCallCount()
|
|
2182
|
+
const retryDecision = decideMissingToolCallRetry({
|
|
2183
|
+
declaredToolCount: declaredTools.length,
|
|
2184
|
+
successfulToolCallCount: toolParser.getSuccessfulToolCallCount(),
|
|
2185
|
+
malformedToolCallCount: toolParser.getMalformedToolCalls().length,
|
|
2186
|
+
emittedToolCallCount: toolParser.getEmittedToolCallCount(),
|
|
2187
|
+
text: finalContent,
|
|
2188
|
+
});
|
|
2189
|
+
const allToolsFailed = retryDecision.retry;
|
|
2170
2190
|
if (!allToolsFailed) break;
|
|
2171
2191
|
|
|
2172
2192
|
const malformedCalls = toolParser.getMalformedToolCalls();
|
|
@@ -2185,7 +2205,9 @@ export async function processStreamingResponse(
|
|
|
2185
2205
|
: "";
|
|
2186
2206
|
|
|
2187
2207
|
let errorMessage: string;
|
|
2188
|
-
if (
|
|
2208
|
+
if (retryDecision.reason === "no_call") {
|
|
2209
|
+
errorMessage = `You did not emit any tool call. Respond with a single tool-call block using one of the declared tools.${toolsHint}`;
|
|
2210
|
+
} else if (undeclaredNames.length > 0) {
|
|
2189
2211
|
errorMessage = `Your previous ${malformedCount} tool call(s) used undeclared tool names: ${undeclaredNames.join(", ")}. Only declared tools can be executed. Please retry with valid tool names.${toolsHint}`;
|
|
2190
2212
|
} else {
|
|
2191
2213
|
const previews = malformedCalls
|
|
@@ -2270,7 +2292,7 @@ export async function processStreamingResponse(
|
|
|
2270
2292
|
}
|
|
2271
2293
|
|
|
2272
2294
|
console.log(
|
|
2273
|
-
`🔄 [Chat] Auto-retry (${malformedRetryCount + 1}/${maxMalformedRetries}) | ${newStreamResult.activeAccountLabel} | ${body.model} | chat=${newStreamResult.uiSessionId.substring(0, 12)} | reason
|
|
2295
|
+
`🔄 [Chat] Auto-retry (${malformedRetryCount + 1}/${maxMalformedRetries}) | ${newStreamResult.activeAccountLabel} | ${body.model} | chat=${newStreamResult.uiSessionId.substring(0, 12)} | reason=${retryDecision.reason === "no_call" ? "missing_tool_call" : "malformed_tool_calls"}`,
|
|
2274
2296
|
);
|
|
2275
2297
|
|
|
2276
2298
|
// Transfer the retry's stream registry entry to the original
|
|
@@ -2809,28 +2831,41 @@ export async function processStreamingResponse(
|
|
|
2809
2831
|
logger.error("[Chat] Stream callback error", errorDetails);
|
|
2810
2832
|
}
|
|
2811
2833
|
// The HTTP response is already committed at this point.
|
|
2812
|
-
// 1. Emit an explicit assistant message delta
|
|
2813
|
-
//
|
|
2814
|
-
//
|
|
2815
|
-
// 2.
|
|
2834
|
+
// 1. Emit an explicit assistant message delta ONLY if this was an upstream content moderation /
|
|
2835
|
+
// policy violation (with finish_reason: "content_filter"), so the user understands why the
|
|
2836
|
+
// prompt was blocked.
|
|
2837
|
+
// 2. For network/transient/quota errors, NEVER emit fake assistant content with finish_reason: "stop".
|
|
2838
|
+
// Doing so masks network failures as successful AI answers, preventing client agent retries and
|
|
2839
|
+
// poisoning chat history with error banners.
|
|
2840
|
+
// 3. Emit a terminal OpenAI-compatible SSE error event + [DONE].
|
|
2816
2841
|
try {
|
|
2817
|
-
const
|
|
2818
|
-
const
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2842
|
+
const isNetwork = isNetworkLikeError(err);
|
|
2843
|
+
const isModeration =
|
|
2844
|
+
normalizedErrorCode === "data_inspection_failed" ||
|
|
2845
|
+
normalizedErrorCode === "fail_sys_user_validate" ||
|
|
2846
|
+
err.message?.toLowerCase().includes("content moderation") ||
|
|
2847
|
+
err.message?.toLowerCase().includes("security verification");
|
|
2848
|
+
|
|
2849
|
+
if (!isNetwork && isModeration) {
|
|
2850
|
+
const userFriendlyNotice = `\n\n⚠️ **[Qwen Moderação / Segurança]** ${err.message || "A resposta foi interrompida pelas políticas de segurança do Qwen."}\n\n`;
|
|
2851
|
+
const errorDelta = {
|
|
2852
|
+
id: completionId,
|
|
2853
|
+
object: "chat.completion.chunk",
|
|
2854
|
+
created: Math.floor(Date.now() / 1000),
|
|
2855
|
+
model: body.model,
|
|
2856
|
+
choices: [
|
|
2857
|
+
{
|
|
2858
|
+
index: 0,
|
|
2859
|
+
delta: { content: userFriendlyNotice },
|
|
2860
|
+
logprobs: null,
|
|
2861
|
+
finish_reason: "content_filter",
|
|
2862
|
+
},
|
|
2863
|
+
],
|
|
2864
|
+
};
|
|
2865
|
+
|
|
2866
|
+
await errorStream.write(`data: ${JSON.stringify(errorDelta)}\n\n`);
|
|
2867
|
+
}
|
|
2832
2868
|
|
|
2833
|
-
await errorStream.write(`data: ${JSON.stringify(errorDelta)}\n\n`);
|
|
2834
2869
|
await errorStream.write(
|
|
2835
2870
|
`data: ${JSON.stringify({
|
|
2836
2871
|
error: {
|
|
@@ -372,7 +372,10 @@ export function chatCompletionsToResponses(
|
|
|
372
372
|
cached_tokens: (chatRes.usage as any).prompt_tokens_details?.cached_tokens ?? 0,
|
|
373
373
|
},
|
|
374
374
|
output_tokens_details: {
|
|
375
|
-
reasoning_tokens:
|
|
375
|
+
reasoning_tokens:
|
|
376
|
+
(chatRes.usage as any).completion_tokens_details?.reasoning_tokens ??
|
|
377
|
+
(chatRes.usage as any).output_tokens_details?.reasoning_tokens ??
|
|
378
|
+
0,
|
|
376
379
|
},
|
|
377
380
|
...(chatRes.usage.context_meter
|
|
378
381
|
? { context_meter: chatRes.usage.context_meter }
|
|
@@ -189,7 +189,7 @@ export function enrichUsageWithContextMeter(
|
|
|
189
189
|
? usage.prompt_tokens
|
|
190
190
|
: null;
|
|
191
191
|
const reportedPromptTokens = options.reportUsage
|
|
192
|
-
? (upstreamPromptTokens ??
|
|
192
|
+
? (upstreamPromptTokens ?? usage.prompt_tokens)
|
|
193
193
|
: usage.prompt_tokens;
|
|
194
194
|
const contextMeter: ContextMeterSnapshot = {
|
|
195
195
|
...snapshot,
|
|
@@ -83,6 +83,7 @@ import { setWafContextResetListener } from "../core/waf-isolation.ts";
|
|
|
83
83
|
import { updateQwenWebVersion, getQwenWebVersion } from "./qwen-headers.ts";
|
|
84
84
|
import { getAccountProfilePath, getProfilesDir } from "../core/paths.ts";
|
|
85
85
|
import { parseJwtExpiry, isTokenExpiringSoon } from "../utils/jwt.ts";
|
|
86
|
+
import { formatAsciiTimezone } from "../utils/timezone.ts";
|
|
86
87
|
|
|
87
88
|
type ContextInitHook = (context: BrowserContext) => Promise<void> | void;
|
|
88
89
|
const contextInitHooks: ContextInitHook[] = [];
|
|
@@ -517,7 +518,7 @@ const headerCaches = new Map<string, AccountHeaderCache>();
|
|
|
517
518
|
// across the browser session; cookies are dynamically updated from the browser context.
|
|
518
519
|
const HEADER_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
|
519
520
|
const HEADER_REFRESH_THRESHOLD = 0.8; // Background refresh at 80% of TTL
|
|
520
|
-
const COOKIE_CACHE_TTL =
|
|
521
|
+
const COOKIE_CACHE_TTL = 60 * 1000; // 1 minute (prevents returning stale expiring token after refresh)
|
|
521
522
|
const cookieCaches = new Map<string, { cookie: string; timestamp: number }>();
|
|
522
523
|
const lastAccountActivity = new Map<string, number>();
|
|
523
524
|
const lastKeepAliveNavigation = new Map<string, number>();
|
|
@@ -1528,6 +1529,14 @@ export async function initPlaywrightForAccount(
|
|
|
1528
1529
|
return;
|
|
1529
1530
|
}
|
|
1530
1531
|
|
|
1532
|
+
let resolveInit!: () => void;
|
|
1533
|
+
let rejectInit!: (err: any) => void;
|
|
1534
|
+
const inFlightDeferred = new Promise<void>((res, rej) => {
|
|
1535
|
+
resolveInit = res;
|
|
1536
|
+
rejectInit = rej;
|
|
1537
|
+
});
|
|
1538
|
+
inFlightAccountInits.set(account.id, inFlightDeferred);
|
|
1539
|
+
|
|
1531
1540
|
const initPromise = (async () => {
|
|
1532
1541
|
const release = await acquireAccountMutex(
|
|
1533
1542
|
account.id,
|
|
@@ -1535,10 +1544,10 @@ export async function initPlaywrightForAccount(
|
|
|
1535
1544
|
Math.max(PLAYWRIGHT_MUTEX_WAIT_MS, 120_000),
|
|
1536
1545
|
);
|
|
1537
1546
|
try {
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1547
|
+
// Double-check after acquiring lock
|
|
1548
|
+
if (accountPages.has(account.id)) {
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1542
1551
|
|
|
1543
1552
|
// If a context limit is configured, make room by closing idle contexts.
|
|
1544
1553
|
await evictIdlePlaywrightContextsToLimit().catch(() => {});
|
|
@@ -1777,6 +1786,26 @@ export async function initPlaywrightForAccount(
|
|
|
1777
1786
|
}
|
|
1778
1787
|
}
|
|
1779
1788
|
}
|
|
1789
|
+
|
|
1790
|
+
// If the session restored from SQLite has an expired access token (e.g. was in standby for hours/days),
|
|
1791
|
+
// proactively refresh it in-page now with the 30-day refresh_token instead of waiting for the first chat
|
|
1792
|
+
// request to hit an upstream 401 and trigger a slow re-auth / captcha loop.
|
|
1793
|
+
const currentCache = getHeaderCache(account.id);
|
|
1794
|
+
const currentCookie = currentCache?.headers?.cookie || "";
|
|
1795
|
+
if (currentCookie && isTokenExpiringSoon(currentCookie, 5)) {
|
|
1796
|
+
try {
|
|
1797
|
+
const refreshRes = await refreshAccountToken(account.id);
|
|
1798
|
+
if (refreshRes.success) {
|
|
1799
|
+
console.log(
|
|
1800
|
+
`⚡ [Playwright] Silently refreshed expired token restored from DB for ${maskEmail(account.email)}`,
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
} catch (err: any) {
|
|
1804
|
+
console.warn(
|
|
1805
|
+
`⚠️ [Playwright] Failed to proactively refresh expired token from DB for ${maskEmail(account.email)}: ${err?.message || err}`,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1780
1809
|
return;
|
|
1781
1810
|
}
|
|
1782
1811
|
|
|
@@ -1845,9 +1874,12 @@ export async function initPlaywrightForAccount(
|
|
|
1845
1874
|
}
|
|
1846
1875
|
})();
|
|
1847
1876
|
|
|
1848
|
-
inFlightAccountInits.set(account.id, initPromise);
|
|
1849
1877
|
try {
|
|
1850
1878
|
await initPromise;
|
|
1879
|
+
resolveInit();
|
|
1880
|
+
} catch (error) {
|
|
1881
|
+
rejectInit(error);
|
|
1882
|
+
throw error;
|
|
1851
1883
|
} finally {
|
|
1852
1884
|
inFlightAccountInits.delete(account.id);
|
|
1853
1885
|
}
|
|
@@ -2160,7 +2192,7 @@ export async function startEmailOtpLogin(
|
|
|
2160
2192
|
email: normalizedEmail,
|
|
2161
2193
|
requestId,
|
|
2162
2194
|
version: getQwenWebVersion() || "0.3.12",
|
|
2163
|
-
timezone:
|
|
2195
|
+
timezone: formatAsciiTimezone(),
|
|
2164
2196
|
},
|
|
2165
2197
|
).catch((err: any) => ({ ok: false, error: err?.message || String(err) }));
|
|
2166
2198
|
|
|
@@ -2272,7 +2304,7 @@ export async function verifyEmailOtpLogin(
|
|
|
2272
2304
|
code: cleanCode,
|
|
2273
2305
|
requestId,
|
|
2274
2306
|
version: getQwenWebVersion() || "0.3.12",
|
|
2275
|
-
timezone:
|
|
2307
|
+
timezone: formatAsciiTimezone(),
|
|
2276
2308
|
},
|
|
2277
2309
|
).catch((err: any) => ({ ok: false, error: err?.message || String(err) }));
|
|
2278
2310
|
|
|
@@ -2626,7 +2658,7 @@ async function loginViaApi(
|
|
|
2626
2658
|
|
|
2627
2659
|
const evalRes = await page
|
|
2628
2660
|
.evaluate(
|
|
2629
|
-
async ({ email, password, signinUrl, requestId }) => {
|
|
2661
|
+
async ({ email, password, signinUrl, requestId, tz }) => {
|
|
2630
2662
|
try {
|
|
2631
2663
|
const response = await fetch(signinUrl, {
|
|
2632
2664
|
method: "POST",
|
|
@@ -2638,7 +2670,7 @@ async function loginViaApi(
|
|
|
2638
2670
|
source: "web",
|
|
2639
2671
|
version: "0.3.11",
|
|
2640
2672
|
"x-request-origin": "https://chat.qwen.ai",
|
|
2641
|
-
timezone:
|
|
2673
|
+
timezone: tz,
|
|
2642
2674
|
"x-request-id": requestId,
|
|
2643
2675
|
},
|
|
2644
2676
|
body: JSON.stringify({ email, password }),
|
|
@@ -2649,7 +2681,7 @@ async function loginViaApi(
|
|
|
2649
2681
|
return { ok: false, error: e.message };
|
|
2650
2682
|
}
|
|
2651
2683
|
},
|
|
2652
|
-
{ email, password: hashedPassword, signinUrl, requestId },
|
|
2684
|
+
{ email, password: hashedPassword, signinUrl, requestId, tz: formatAsciiTimezone() },
|
|
2653
2685
|
)
|
|
2654
2686
|
.catch(() => null);
|
|
2655
2687
|
|
|
@@ -2667,7 +2699,7 @@ async function loginViaApi(
|
|
|
2667
2699
|
const fallbackUrl = qwenUrl("/api/v2/auths/signin");
|
|
2668
2700
|
const fallbackRes = await page
|
|
2669
2701
|
.evaluate(
|
|
2670
|
-
async ({ email, password, fallbackUrl, requestId }) => {
|
|
2702
|
+
async ({ email, password, fallbackUrl, requestId, tz }) => {
|
|
2671
2703
|
try {
|
|
2672
2704
|
const response = await fetch(fallbackUrl, {
|
|
2673
2705
|
method: "POST",
|
|
@@ -2678,7 +2710,7 @@ async function loginViaApi(
|
|
|
2678
2710
|
"content-type": "application/json",
|
|
2679
2711
|
source: "web",
|
|
2680
2712
|
version: "0.3.11",
|
|
2681
|
-
timezone:
|
|
2713
|
+
timezone: tz,
|
|
2682
2714
|
"x-request-id": requestId,
|
|
2683
2715
|
},
|
|
2684
2716
|
body: JSON.stringify({ email, password }),
|
|
@@ -2689,7 +2721,7 @@ async function loginViaApi(
|
|
|
2689
2721
|
return { ok: false, error: e.message };
|
|
2690
2722
|
}
|
|
2691
2723
|
},
|
|
2692
|
-
{ email, password: hashedPassword, fallbackUrl, requestId },
|
|
2724
|
+
{ email, password: hashedPassword, fallbackUrl, requestId, tz: formatAsciiTimezone() },
|
|
2693
2725
|
)
|
|
2694
2726
|
.catch(() => null);
|
|
2695
2727
|
|
|
@@ -3111,7 +3143,7 @@ export async function refreshAccountToken(
|
|
|
3111
3143
|
if (refreshToken) {
|
|
3112
3144
|
try {
|
|
3113
3145
|
const refreshResult = await page.evaluate(
|
|
3114
|
-
async ({ rTok }) => {
|
|
3146
|
+
async ({ rTok, tz }) => {
|
|
3115
3147
|
try {
|
|
3116
3148
|
const response = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
|
|
3117
3149
|
method: "GET",
|
|
@@ -3121,7 +3153,7 @@ export async function refreshAccountToken(
|
|
|
3121
3153
|
source: "web",
|
|
3122
3154
|
version: "0.3.11",
|
|
3123
3155
|
"x-request-origin": "https://chat.qwen.ai",
|
|
3124
|
-
timezone:
|
|
3156
|
+
timezone: tz,
|
|
3125
3157
|
authorization: `Bearer ${rTok}`,
|
|
3126
3158
|
},
|
|
3127
3159
|
});
|
|
@@ -3131,7 +3163,7 @@ export async function refreshAccountToken(
|
|
|
3131
3163
|
return { ok: false, error: e.message };
|
|
3132
3164
|
}
|
|
3133
3165
|
},
|
|
3134
|
-
{ rTok: refreshToken },
|
|
3166
|
+
{ rTok: refreshToken, tz: formatAsciiTimezone() },
|
|
3135
3167
|
);
|
|
3136
3168
|
|
|
3137
3169
|
const json = refreshResult?.data;
|
|
@@ -3175,6 +3207,7 @@ export async function refreshAccountToken(
|
|
|
3175
3207
|
const cache = getHeaderCache(accountId);
|
|
3176
3208
|
cache.headers.cookie = cookieStr;
|
|
3177
3209
|
cache.lastRefresh = Date.now();
|
|
3210
|
+
cookieCaches.set(accountId, { cookie: cookieStr, timestamp: Date.now() });
|
|
3178
3211
|
markAccountHeadersReady(accountId);
|
|
3179
3212
|
|
|
3180
3213
|
// Update SQLite session with new tokens
|
|
@@ -3213,6 +3246,7 @@ export async function refreshAccountToken(
|
|
|
3213
3246
|
const cache = getHeaderCache(accountId);
|
|
3214
3247
|
cache.headers.cookie = cookieStr;
|
|
3215
3248
|
cache.lastRefresh = Date.now();
|
|
3249
|
+
cookieCaches.set(accountId, { cookie: cookieStr, timestamp: Date.now() });
|
|
3216
3250
|
markAccountHeadersReady(accountId);
|
|
3217
3251
|
saveAuthSession(accountId, {
|
|
3218
3252
|
cookie: cookieStr,
|
|
@@ -4653,11 +4687,11 @@ export async function keepAlivePlaywrightAccount(
|
|
|
4653
4687
|
|
|
4654
4688
|
// Proactive session check: if token expires within 5 minutes, refresh in-page silently using refreshAccountToken
|
|
4655
4689
|
const lastRefreshAttempt = lastProactiveTokenRefresh.get(accountId) ?? 0;
|
|
4656
|
-
const cookie = await getCookies(accountId);
|
|
4690
|
+
const cookie = await getCookies(accountId, true);
|
|
4657
4691
|
if (cookie && isTokenExpiringSoon(cookie, 5) && now - lastRefreshAttempt > 60_000) {
|
|
4658
4692
|
lastProactiveTokenRefresh.set(accountId, now);
|
|
4659
|
-
|
|
4660
|
-
|
|
4693
|
+
logger.debug(
|
|
4694
|
+
`[SessionKeeper] Account ${accountId} access token expiring soon (<5m); silently refreshing...`,
|
|
4661
4695
|
);
|
|
4662
4696
|
const res = await refreshAccountToken(accountId);
|
|
4663
4697
|
if (res.success) {
|
|
@@ -4665,6 +4699,9 @@ export async function keepAlivePlaywrightAccount(
|
|
|
4665
4699
|
touchAccountActivity(accountId);
|
|
4666
4700
|
return true;
|
|
4667
4701
|
}
|
|
4702
|
+
logger.warn(
|
|
4703
|
+
`[SessionKeeper] Failed to silently refresh expiring token for ${accountId}: ${res.error || "unknown error"}`,
|
|
4704
|
+
);
|
|
4668
4705
|
}
|
|
4669
4706
|
|
|
4670
4707
|
const lastNavigation = lastKeepAliveNavigation.get(accountId) ?? 0;
|
|
@@ -2,6 +2,7 @@ import { v4 as uuidv4 } from "uuid";
|
|
|
2
2
|
import { qwenUrl, qwenOrigin } from "./qwen-url.ts";
|
|
3
3
|
import { config } from "../core/config.js";
|
|
4
4
|
import { getChromeMajor, getSystemLocale } from "./fingerprint.ts";
|
|
5
|
+
import { formatAsciiTimezone } from "../utils/timezone.ts";
|
|
5
6
|
|
|
6
7
|
export function getSystemAcceptLanguage(customLocale?: string): string {
|
|
7
8
|
const locale = customLocale || getSystemLocale();
|
|
@@ -79,8 +80,8 @@ export function buildQwenRequestHeaders(
|
|
|
79
80
|
"bx-v": opts.bxV || "2.5.37",
|
|
80
81
|
source: "web",
|
|
81
82
|
version: opts.version || getQwenWebVersion(),
|
|
82
|
-
timezone: opts.extra?.timezone || opts.extra?.Timezone ||
|
|
83
|
-
Timezone: opts.extra?.Timezone || opts.extra?.timezone ||
|
|
83
|
+
timezone: opts.extra?.timezone || opts.extra?.Timezone || formatAsciiTimezone(),
|
|
84
|
+
Timezone: opts.extra?.Timezone || opts.extra?.timezone || formatAsciiTimezone(),
|
|
84
85
|
// Use the real browser client-hints when captured (anti-hardcoded); fall
|
|
85
86
|
// back to the static fingerprint otherwise.
|
|
86
87
|
"sec-ch-ua":
|