qwenproxy-cli 1.4.8 → 1.5.0

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.
@@ -1915,7 +1915,15 @@ function defaultCooldownDurationMs() {
1915
1915
  return computeQuotaCooldownMs(Date.now());
1916
1916
  }
1917
1917
  function markAccountRateLimited(accountId, cooldownMs, reason, options = {}) {
1918
+ if (cooldownMs !== void 0 && cooldownMs <= 0) {
1919
+ clearAccountCooldown(accountId);
1920
+ return;
1921
+ }
1918
1922
  const duration = cooldownMs ?? defaultCooldownDurationMs();
1923
+ if (duration <= 0) {
1924
+ clearAccountCooldown(accountId);
1925
+ return;
1926
+ }
1919
1927
  const until = Date.now() + duration;
1920
1928
  const cooldownReason = reason ?? "RateLimited";
1921
1929
  cooldowns.set(accountId, {
@@ -1938,6 +1946,19 @@ function markAccountRateLimited(accountId, cooldownMs, reason, options = {}) {
1938
1946
  );
1939
1947
  }
1940
1948
  }
1949
+ function clearAccountCooldown(accountId) {
1950
+ cooldowns.delete(accountId);
1951
+ if (accountId !== "global") {
1952
+ try {
1953
+ updateAccountCooldown(accountId, 0, null);
1954
+ } catch (err) {
1955
+ console.error(
1956
+ `\u274C [AccountManager] Failed to clear cooldown in DB for ${accountId}:`,
1957
+ err.message
1958
+ );
1959
+ }
1960
+ }
1961
+ }
1941
1962
  function markAccountHeadersReady(accountId) {
1942
1963
  if (!accountId || accountId === "global") return;
1943
1964
  headersReadyAccounts.add(accountId);
@@ -1978,7 +1978,15 @@ function defaultCooldownDurationMs() {
1978
1978
  return computeQuotaCooldownMs(Date.now());
1979
1979
  }
1980
1980
  function markAccountRateLimited(accountId, cooldownMs, reason, options = {}) {
1981
+ if (cooldownMs !== void 0 && cooldownMs <= 0) {
1982
+ clearAccountCooldown(accountId);
1983
+ return;
1984
+ }
1981
1985
  const duration = cooldownMs ?? defaultCooldownDurationMs();
1986
+ if (duration <= 0) {
1987
+ clearAccountCooldown(accountId);
1988
+ return;
1989
+ }
1982
1990
  const until = Date.now() + duration;
1983
1991
  const cooldownReason = reason ?? "RateLimited";
1984
1992
  cooldowns.set(accountId, {
@@ -2001,6 +2009,19 @@ function markAccountRateLimited(accountId, cooldownMs, reason, options = {}) {
2001
2009
  );
2002
2010
  }
2003
2011
  }
2012
+ function clearAccountCooldown(accountId) {
2013
+ cooldowns.delete(accountId);
2014
+ if (accountId !== "global") {
2015
+ try {
2016
+ updateAccountCooldown(accountId, 0, null);
2017
+ } catch (err) {
2018
+ console.error(
2019
+ `\u274C [AccountManager] Failed to clear cooldown in DB for ${accountId}:`,
2020
+ err.message
2021
+ );
2022
+ }
2023
+ }
2024
+ }
2004
2025
  function markAccountHeadersReady(accountId) {
2005
2026
  if (!accountId || accountId === "global") return;
2006
2027
  headersReadyAccounts.add(accountId);
@@ -2769,7 +2790,8 @@ function buildQwenRequestHeaders(opts) {
2769
2790
  "bx-v": opts.bxV || "2.5.37",
2770
2791
  source: "web",
2771
2792
  version: opts.version || getQwenWebVersion(),
2772
- timezone: opts.extra?.timezone || (/* @__PURE__ */ new Date()).toString().split(" (")[0],
2793
+ timezone: opts.extra?.timezone || opts.extra?.Timezone || (/* @__PURE__ */ new Date()).toString().split(" (")[0],
2794
+ Timezone: opts.extra?.Timezone || opts.extra?.timezone || (/* @__PURE__ */ new Date()).toString().split(" (")[0],
2773
2795
  // Use the real browser client-hints when captured (anti-hardcoded); fall
2774
2796
  // back to the static fingerprint otherwise.
2775
2797
  "sec-ch-ua": opts.secChUa || `"Google Chrome";v="${getChromeMajor()}", "Chromium";v="${getChromeMajor()}", "Not.A/Brand";v="99"`,
@@ -7623,13 +7645,15 @@ async function createQwenChatSession(headers, model, accountId, chatMode = "thre
7623
7645
  } catch {
7624
7646
  }
7625
7647
  }
7626
- if (json?.data?.code === "Unauthorized" || json?.code === "Unauthorized" || rawLower.includes("unauthorized") || rawLower.includes("permission to access")) {
7648
+ const isUnauthorized = json?.data?.code?.toLowerCase() === "unauthorized" || json?.code?.toLowerCase() === "unauthorized" || rawLower.includes("unauthorized") || rawLower.includes("permission to access") || rawLower.includes("token has expired") || rawLower.includes("401");
7649
+ if (isUnauthorized) {
7627
7650
  if (accountId) {
7628
7651
  try {
7629
7652
  const { refreshAccountToken: refreshAccountToken2 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
7630
7653
  const refreshed = await refreshAccountToken2(accountId);
7631
7654
  if (refreshed.success) {
7632
- return createQwenChatSession(headers, model, accountId, chatMode);
7655
+ const { headers: freshHeaders } = await getQwenHeaders(false, accountId);
7656
+ return createQwenChatSession(freshHeaders, model, accountId, chatMode);
7633
7657
  }
7634
7658
  } catch {
7635
7659
  }
@@ -9623,8 +9647,18 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
9623
9647
  );
9624
9648
  }
9625
9649
  };
9650
+ let forceRefresh = options?.forceNewChat === true;
9651
+ if (!forceRefresh && accountId) {
9652
+ try {
9653
+ const basic = await getBasicHeaders2(accountId);
9654
+ if (isTokenExpiringSoon(basic.cookie, 3)) {
9655
+ forceRefresh = true;
9656
+ }
9657
+ } catch {
9658
+ }
9659
+ }
9626
9660
  const captured = await getQwenHeaders(
9627
- options?.forceNewChat === true,
9661
+ forceRefresh,
9628
9662
  accountId
9629
9663
  );
9630
9664
  ensureNotAborted();
@@ -9638,7 +9672,7 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
9638
9672
  if (options && "chatSessionId" in options) {
9639
9673
  if (options.chatSessionId === null || options.chatSessionId === "") {
9640
9674
  const acquired = await acquireNewQwenChatSession(
9641
- headers,
9675
+ activeHeaders,
9642
9676
  model,
9643
9677
  accountId,
9644
9678
  options?.chatMode ?? "thread"
@@ -9646,6 +9680,11 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
9646
9680
  chatSessionId = acquired.chatId;
9647
9681
  leasedWarmChat = acquired.leasedFromPool;
9648
9682
  createdNewChat = true;
9683
+ try {
9684
+ const fresh = await getQwenHeaders(false, accountId);
9685
+ activeHeaders = fresh.headers;
9686
+ } catch {
9687
+ }
9649
9688
  } else {
9650
9689
  chatSessionId = options.chatSessionId;
9651
9690
  }
@@ -9653,7 +9692,7 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
9653
9692
  chatSessionId = captured.chatSessionId;
9654
9693
  if (!chatSessionId) {
9655
9694
  const acquired = await acquireNewQwenChatSession(
9656
- headers,
9695
+ activeHeaders,
9657
9696
  model,
9658
9697
  accountId,
9659
9698
  options?.chatMode ?? "thread"
@@ -9661,6 +9700,11 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
9661
9700
  chatSessionId = acquired.chatId;
9662
9701
  leasedWarmChat = acquired.leasedFromPool;
9663
9702
  createdNewChat = true;
9703
+ try {
9704
+ const fresh = await getQwenHeaders(false, accountId);
9705
+ activeHeaders = fresh.headers;
9706
+ } catch {
9707
+ }
9664
9708
  }
9665
9709
  }
9666
9710
  ensureNotAborted();
package/dist/index.js CHANGED
@@ -2986,7 +2986,15 @@ function defaultCooldownDurationMs() {
2986
2986
  return computeQuotaCooldownMs(Date.now());
2987
2987
  }
2988
2988
  function markAccountRateLimited(accountId, cooldownMs, reason, options = {}) {
2989
+ if (cooldownMs !== void 0 && cooldownMs <= 0) {
2990
+ clearAccountCooldown(accountId);
2991
+ return;
2992
+ }
2989
2993
  const duration = cooldownMs ?? defaultCooldownDurationMs();
2994
+ if (duration <= 0) {
2995
+ clearAccountCooldown(accountId);
2996
+ return;
2997
+ }
2990
2998
  const until = Date.now() + duration;
2991
2999
  const cooldownReason = reason ?? "RateLimited";
2992
3000
  cooldowns.set(accountId, {
@@ -3986,7 +3994,8 @@ function buildQwenRequestHeaders(opts) {
3986
3994
  "bx-v": opts.bxV || "2.5.37",
3987
3995
  source: "web",
3988
3996
  version: opts.version || getQwenWebVersion(),
3989
- timezone: opts.extra?.timezone || (/* @__PURE__ */ new Date()).toString().split(" (")[0],
3997
+ timezone: opts.extra?.timezone || opts.extra?.Timezone || (/* @__PURE__ */ new Date()).toString().split(" (")[0],
3998
+ Timezone: opts.extra?.Timezone || opts.extra?.timezone || (/* @__PURE__ */ new Date()).toString().split(" (")[0],
3990
3999
  // Use the real browser client-hints when captured (anti-hardcoded); fall
3991
4000
  // back to the static fingerprint otherwise.
3992
4001
  "sec-ch-ua": opts.secChUa || `"Google Chrome";v="${getChromeMajor()}", "Chromium";v="${getChromeMajor()}", "Not.A/Brand";v="99"`,
@@ -9087,8 +9096,11 @@ ${thought}`);
9087
9096
  }
9088
9097
  return sections.join("\n\n");
9089
9098
  }
9099
+ function isAnswerPhase(phase) {
9100
+ return typeof phase === "string" && ANSWER_PHASES.has(phase);
9101
+ }
9090
9102
  function isThinkingPhase(phase) {
9091
- return phase === "think" || phase === "thinking_summary";
9103
+ return typeof phase === "string" && THINKING_PHASES.has(phase);
9092
9104
  }
9093
9105
  function extractThinkingContent(delta) {
9094
9106
  if (Array.isArray(delta?.extra?.summary_title?.content) || Array.isArray(delta?.extra?.summary_thought?.content)) {
@@ -9179,9 +9191,25 @@ function buildUsage(accumulator) {
9179
9191
  }
9180
9192
  return usage;
9181
9193
  }
9194
+ var ANSWER_PHASES, THINKING_PHASES;
9182
9195
  var init_helpers = __esm({
9183
9196
  "src/routes/chat/helpers.ts"() {
9184
9197
  "use strict";
9198
+ ANSWER_PHASES = /* @__PURE__ */ new Set([
9199
+ "answer",
9200
+ "deep_research_answer",
9201
+ "ReportGeneration",
9202
+ "PdfMdGen",
9203
+ "image",
9204
+ "image_generation",
9205
+ "image_gen",
9206
+ "t2i"
9207
+ ]);
9208
+ THINKING_PHASES = /* @__PURE__ */ new Set([
9209
+ "think",
9210
+ "thinking_summary",
9211
+ "DeepThinking"
9212
+ ]);
9185
9213
  }
9186
9214
  });
9187
9215
 
@@ -9355,7 +9383,7 @@ function classifyRetryAction(err, options) {
9355
9383
  switchAccount: true,
9356
9384
  forceNewChat: true,
9357
9385
  retryAfterMs: baseDelayMs,
9358
- accountCooldownMs: config.concurrency.initFailureCooldownMs,
9386
+ accountCooldownMs: 0,
9359
9387
  accountCooldownReason: "PersonalizationFailed"
9360
9388
  });
9361
9389
  }
@@ -9365,8 +9393,8 @@ function classifyRetryAction(err, options) {
9365
9393
  forceNewChat: true,
9366
9394
  retryWithFullPrompt: true,
9367
9395
  retryAfterMs: Math.min(baseDelayMs, 1e3),
9368
- accountCooldownMs: config.concurrency.initFailureCooldownMs,
9369
- accountCooldownReason: "AuthInitFailed"
9396
+ accountCooldownMs: 0,
9397
+ accountCooldownReason: "AuthExpired"
9370
9398
  });
9371
9399
  }
9372
9400
  if (isCorruptedChatHistoryError(err)) {
@@ -9783,8 +9811,8 @@ async function requestCompletionsInBrowser(params) {
9783
9811
  const choice = chunk?.choices?.[0];
9784
9812
  const delta = choice?.delta;
9785
9813
  const phase = delta?.phase;
9786
- const isAnswerPhase = !phase || phase === "answer";
9787
- if (choice?.finish_reason || delta?.status === "finished" && isAnswerPhase) {
9814
+ const isAnswerPhase2 = !phase || phase === "answer";
9815
+ if (choice?.finish_reason || delta?.status === "finished" && isAnswerPhase2) {
9788
9816
  finished = true;
9789
9817
  break;
9790
9818
  }
@@ -10835,13 +10863,15 @@ async function createQwenChatSession(headers, model, accountId, chatMode = "thre
10835
10863
  } catch {
10836
10864
  }
10837
10865
  }
10838
- if (json?.data?.code === "Unauthorized" || json?.code === "Unauthorized" || rawLower.includes("unauthorized") || rawLower.includes("permission to access")) {
10866
+ const isUnauthorized = json?.data?.code?.toLowerCase() === "unauthorized" || json?.code?.toLowerCase() === "unauthorized" || rawLower.includes("unauthorized") || rawLower.includes("permission to access") || rawLower.includes("token has expired") || rawLower.includes("401");
10867
+ if (isUnauthorized) {
10839
10868
  if (accountId) {
10840
10869
  try {
10841
10870
  const { refreshAccountToken: refreshAccountToken2 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
10842
10871
  const refreshed = await refreshAccountToken2(accountId);
10843
10872
  if (refreshed.success) {
10844
- return createQwenChatSession(headers, model, accountId, chatMode);
10873
+ const { headers: freshHeaders } = await getQwenHeaders(false, accountId);
10874
+ return createQwenChatSession(freshHeaders, model, accountId, chatMode);
10845
10875
  }
10846
10876
  } catch {
10847
10877
  }
@@ -12835,8 +12865,18 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
12835
12865
  );
12836
12866
  }
12837
12867
  };
12868
+ let forceRefresh = options?.forceNewChat === true;
12869
+ if (!forceRefresh && accountId) {
12870
+ try {
12871
+ const basic = await getBasicHeaders2(accountId);
12872
+ if (isTokenExpiringSoon(basic.cookie, 3)) {
12873
+ forceRefresh = true;
12874
+ }
12875
+ } catch {
12876
+ }
12877
+ }
12838
12878
  const captured = await getQwenHeaders(
12839
- options?.forceNewChat === true,
12879
+ forceRefresh,
12840
12880
  accountId
12841
12881
  );
12842
12882
  ensureNotAborted();
@@ -12850,7 +12890,7 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
12850
12890
  if (options && "chatSessionId" in options) {
12851
12891
  if (options.chatSessionId === null || options.chatSessionId === "") {
12852
12892
  const acquired = await acquireNewQwenChatSession(
12853
- headers,
12893
+ activeHeaders,
12854
12894
  model,
12855
12895
  accountId,
12856
12896
  options?.chatMode ?? "thread"
@@ -12858,6 +12898,11 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
12858
12898
  chatSessionId = acquired.chatId;
12859
12899
  leasedWarmChat = acquired.leasedFromPool;
12860
12900
  createdNewChat = true;
12901
+ try {
12902
+ const fresh = await getQwenHeaders(false, accountId);
12903
+ activeHeaders = fresh.headers;
12904
+ } catch {
12905
+ }
12861
12906
  } else {
12862
12907
  chatSessionId = options.chatSessionId;
12863
12908
  }
@@ -12865,7 +12910,7 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
12865
12910
  chatSessionId = captured.chatSessionId;
12866
12911
  if (!chatSessionId) {
12867
12912
  const acquired = await acquireNewQwenChatSession(
12868
- headers,
12913
+ activeHeaders,
12869
12914
  model,
12870
12915
  accountId,
12871
12916
  options?.chatMode ?? "thread"
@@ -12873,6 +12918,11 @@ async function createQwenStreamInternal(prompt, enableThinking, modelId, forcedP
12873
12918
  chatSessionId = acquired.chatId;
12874
12919
  leasedWarmChat = acquired.leasedFromPool;
12875
12920
  createdNewChat = true;
12921
+ try {
12922
+ const fresh = await getQwenHeaders(false, accountId);
12923
+ activeHeaders = fresh.headers;
12924
+ } catch {
12925
+ }
12876
12926
  }
12877
12927
  }
12878
12928
  ensureNotAborted();
@@ -14031,10 +14081,12 @@ async function getSTSToken(filename, filesize, filetype, headers, accountId) {
14031
14081
  }
14032
14082
  data = await response.json().catch(() => null);
14033
14083
  }
14034
- const is401 = data?.success === false && (data?.data?.code === "Unauthorized" || typeof data?.data?.details === "string" && data.data.details.includes("401"));
14084
+ const codeLower = (data?.data?.code || data?.code || "").toLowerCase();
14085
+ const detailsLower = (typeof data?.data?.details === "string" ? data.data.details : typeof data?.message === "string" ? data.message : "").toLowerCase();
14086
+ const is401 = data?.success === false && (codeLower === "unauthorized" || detailsLower.includes("401") || detailsLower.includes("token has expired") || detailsLower.includes("unauthorized"));
14035
14087
  if (is401) {
14036
14088
  try {
14037
- const { refreshHeaders: refreshHeaders2 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
14089
+ const { refreshAccountToken: refreshAccountToken2 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
14038
14090
  const { getBasicHeaders: getBasicHeaders3 } = await Promise.resolve().then(() => (init_auth_playwright(), auth_playwright_exports));
14039
14091
  const { loadAccounts: loadAccounts3 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
14040
14092
  const resolvedId2 = accountId ?? loadAccounts3()[0]?.id;
@@ -14042,16 +14094,18 @@ async function getSTSToken(filename, filesize, filetype, headers, accountId) {
14042
14094
  console.warn(
14043
14095
  `[Upload] STS token 401 \u2014 refreshing session with re-auth and retrying...`
14044
14096
  );
14045
- await refreshHeaders2(resolvedId2, void 0, true);
14046
- const fresh = await getBasicHeaders3(resolvedId2);
14047
- headers.cookie = fresh.cookie;
14048
- headers["user-agent"] = fresh.userAgent;
14049
- headers["bx-v"] = fresh.bxV;
14050
- if (fresh.bxUa) headers["bx-ua"] = fresh.bxUa;
14051
- if (fresh.bxUmidtoken) headers["bx-umidtoken"] = fresh.bxUmidtoken;
14052
- const retryRes = await doFetch(headers);
14053
- if (retryRes.ok) {
14054
- data = await retryRes.json().catch(() => null);
14097
+ const refreshResult = await refreshAccountToken2(resolvedId2);
14098
+ if (refreshResult.success) {
14099
+ const fresh = await getBasicHeaders3(resolvedId2);
14100
+ headers.cookie = fresh.cookie;
14101
+ headers["user-agent"] = fresh.userAgent;
14102
+ headers["bx-v"] = fresh.bxV;
14103
+ if (fresh.bxUa) headers["bx-ua"] = fresh.bxUa;
14104
+ if (fresh.bxUmidtoken) headers["bx-umidtoken"] = fresh.bxUmidtoken;
14105
+ const retryRes = await doFetch(headers);
14106
+ if (retryRes.ok) {
14107
+ data = await retryRes.json().catch(() => null);
14108
+ }
14055
14109
  }
14056
14110
  }
14057
14111
  } catch (refreshErr) {
@@ -17166,15 +17220,26 @@ ${loopReminder}` : truncation.prompt;
17166
17220
  }
17167
17221
  }
17168
17222
  if (policy.reason === "account_initialization_failed") {
17169
- console.warn(
17170
- `\u26A0\uFE0F [Chat] Account initialization failed | ${currentAccountEmail} | cooldown=${Math.round((policy.accountCooldownMs ?? 0) / 1e3)}s`
17171
- );
17172
- markAccountFailed(currentAccountId);
17173
- markAccountRateLimited(
17174
- currentAccountId,
17175
- policy.accountCooldownMs,
17176
- policy.accountCooldownReason
17177
- );
17223
+ const cooldownMs = policy.accountCooldownMs ?? 0;
17224
+ if (cooldownMs > 0) {
17225
+ console.warn(
17226
+ `\u26A0\uFE0F [Chat] Account initialization failed | ${currentAccountEmail} | cooldown=${Math.round(cooldownMs / 1e3)}s`
17227
+ );
17228
+ markAccountFailed(currentAccountId);
17229
+ markAccountRateLimited(
17230
+ currentAccountId,
17231
+ cooldownMs,
17232
+ policy.accountCooldownReason
17233
+ );
17234
+ } else {
17235
+ console.warn(
17236
+ `\u26A0\uFE0F [Chat] Account auth expired/retryable | ${currentAccountEmail} | rotating without cooldown`
17237
+ );
17238
+ if (policy.accountCooldownReason === "AuthExpired") {
17239
+ Promise.resolve().then(() => (init_playwright(), playwright_exports)).then(({ refreshAccountToken: refreshAccountToken2 }) => refreshAccountToken2(currentAccountId)).catch(() => {
17240
+ });
17241
+ }
17242
+ }
17178
17243
  return { success: false, error: err };
17179
17244
  }
17180
17245
  if (policy.retryable && shouldSwitchAccount && !isSingleAccount && accountSwitches < maxAccountSwitches) {
@@ -17183,7 +17248,7 @@ ${loopReminder}` : truncation.prompt;
17183
17248
  console.warn(
17184
17249
  `\u{1F504} [Chat] Switching account after ${policy.reason} | ${currentAccountEmail} -> ${maskEmail(nextAccount.email)}`
17185
17250
  );
17186
- if (policy.accountCooldownMs || policy.accountCooldownReason) {
17251
+ if ((policy.accountCooldownMs ?? 0) > 0) {
17187
17252
  markAccountRateLimited(
17188
17253
  currentAccountId,
17189
17254
  policy.accountCooldownMs,
@@ -20066,7 +20131,7 @@ async function processNonStreamingResponse(params) {
20066
20131
  foundStr = true;
20067
20132
  }
20068
20133
  }
20069
- } else if (delta.phase === "answer") {
20134
+ } else if (isAnswerPhase(delta.phase)) {
20070
20135
  isThinkingChunk = false;
20071
20136
  if (delta.content !== void 0) {
20072
20137
  const newContent = delta.content || "";
@@ -20805,13 +20870,17 @@ async function processStreamingResponse(params) {
20805
20870
  return false;
20806
20871
  }
20807
20872
  const switchAccount = policy.switchAccount && !retryInvalidInputOnSameAccount;
20808
- if (switchAccount && (policy.accountCooldownMs || policy.accountCooldownReason)) {
20873
+ if (switchAccount && (policy.accountCooldownMs ?? 0) > 0) {
20809
20874
  markAccountRateLimited(
20810
20875
  currentAccountId,
20811
20876
  policy.accountCooldownMs,
20812
20877
  policy.accountCooldownReason || "StreamRetry"
20813
20878
  );
20814
20879
  }
20880
+ if (policy.accountCooldownReason === "AuthExpired") {
20881
+ Promise.resolve().then(() => (init_playwright(), playwright_exports)).then(({ refreshAccountToken: refreshAccountToken2 }) => refreshAccountToken2(currentAccountId)).catch(() => {
20882
+ });
20883
+ }
20815
20884
  retryContext.releaseAccountLease?.();
20816
20885
  retryContext.releaseAccountLease = null;
20817
20886
  await reader.cancel().catch(() => void 0);
@@ -21055,7 +21124,7 @@ async function processStreamingResponse(params) {
21055
21124
  }
21056
21125
  );
21057
21126
  }
21058
- if (delta.phase === "answer" && delta.status === "finished") {
21127
+ if (isAnswerPhase(delta.phase) && delta.status === "finished") {
21059
21128
  upstreamDone = true;
21060
21129
  if (!clientDisconnected) flushWrites();
21061
21130
  break;
@@ -21078,7 +21147,7 @@ async function processStreamingResponse(params) {
21078
21147
  foundStr = true;
21079
21148
  }
21080
21149
  }
21081
- } else if (delta.phase === "answer") {
21150
+ } else if (isAnswerPhase(delta.phase)) {
21082
21151
  isThinkingChunk = false;
21083
21152
  if (delta.content !== void 0) {
21084
21153
  const newContent = delta.content || "";
@@ -21456,7 +21525,7 @@ Please retry your tool call(s) with correct JSON and valid tool names from the a
21456
21525
  if (!delta) {
21457
21526
  continue;
21458
21527
  }
21459
- if (delta.phase === "answer" && delta.status === "finished") {
21528
+ if (isAnswerPhase(delta.phase) && delta.status === "finished") {
21460
21529
  upstreamDone = true;
21461
21530
  break retryReadLoop;
21462
21531
  }
@@ -25650,11 +25719,14 @@ function recordServerLog(level, text) {
25650
25719
  }
25651
25720
  line = line.replace(/^(?:\[?(?:INFO|WARN|WARNING|ERROR|ERR|DEBUG)\]?\s+)+/i, "").replace(/([\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}]\uFE0F?)\s{2,}/gu, "$1 ");
25652
25721
  if (!line) continue;
25653
- const isDuplicate = logHistory.slice(-10).some((entry2) => entry2.time === time && entry2.level === level && entry2.message === line);
25722
+ const now = Date.now();
25723
+ const isDuplicate = logHistory.slice(-10).some(
25724
+ (entry2) => (entry2.time === time || entry2._ts && now - entry2._ts < 1500) && entry2.level === level && entry2.message === line
25725
+ );
25654
25726
  if (isDuplicate) {
25655
25727
  continue;
25656
25728
  }
25657
- const entry = { time, level, message: line };
25729
+ const entry = { time, level, message: line, _ts: now };
25658
25730
  logHistory.push(entry);
25659
25731
  if (logHistory.length > MAX_LOG_HISTORY) {
25660
25732
  logHistory.shift();
@@ -25817,7 +25889,7 @@ function getAppVersion() {
25817
25889
  }
25818
25890
  } catch {
25819
25891
  }
25820
- cachedVersion = "v1.4.8";
25892
+ cachedVersion = "v1.5.0";
25821
25893
  return cachedVersion;
25822
25894
  }
25823
25895
  var cachedVersion;
package/dist/login.js CHANGED
@@ -2016,7 +2016,15 @@ function defaultCooldownDurationMs() {
2016
2016
  return computeQuotaCooldownMs(Date.now());
2017
2017
  }
2018
2018
  function markAccountRateLimited(accountId, cooldownMs, reason, options = {}) {
2019
+ if (cooldownMs !== void 0 && cooldownMs <= 0) {
2020
+ clearAccountCooldown(accountId);
2021
+ return;
2022
+ }
2019
2023
  const duration = cooldownMs ?? defaultCooldownDurationMs();
2024
+ if (duration <= 0) {
2025
+ clearAccountCooldown(accountId);
2026
+ return;
2027
+ }
2020
2028
  const until = Date.now() + duration;
2021
2029
  const cooldownReason = reason ?? "RateLimited";
2022
2030
  cooldowns.set(accountId, {
@@ -1110,7 +1110,15 @@ function defaultCooldownDurationMs() {
1110
1110
  return computeQuotaCooldownMs(Date.now());
1111
1111
  }
1112
1112
  function markAccountRateLimited(accountId, cooldownMs, reason, options = {}) {
1113
+ if (cooldownMs !== void 0 && cooldownMs <= 0) {
1114
+ clearAccountCooldown(accountId);
1115
+ return;
1116
+ }
1113
1117
  const duration = cooldownMs ?? defaultCooldownDurationMs();
1118
+ if (duration <= 0) {
1119
+ clearAccountCooldown(accountId);
1120
+ return;
1121
+ }
1114
1122
  const until = Date.now() + duration;
1115
1123
  const cooldownReason = reason ?? "RateLimited";
1116
1124
  cooldowns.set(accountId, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.4.8",
3
+ "version": "1.5.0",
4
4
  "description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -50,7 +50,16 @@ export function markAccountRateLimited(
50
50
  reason?: string,
51
51
  options: { silent?: boolean } = {},
52
52
  ): void {
53
+ // If cooldown is explicitly 0 or negative, do not park the account.
54
+ if (cooldownMs !== undefined && cooldownMs <= 0) {
55
+ clearAccountCooldown(accountId);
56
+ return;
57
+ }
53
58
  const duration = cooldownMs ?? defaultCooldownDurationMs();
59
+ if (duration <= 0) {
60
+ clearAccountCooldown(accountId);
61
+ return;
62
+ }
54
63
  const until = Date.now() + duration;
55
64
  const cooldownReason = reason ?? "RateLimited";
56
65
 
@@ -11,6 +11,7 @@ export interface ServerLogMessage {
11
11
  time: string;
12
12
  level: "INFO" | "WARN" | "ERROR";
13
13
  message: string;
14
+ _ts?: number;
14
15
  }
15
16
 
16
17
  const MAX_LOG_HISTORY = 2000;
@@ -80,15 +81,21 @@ export function recordServerLog(level: "INFO" | "WARN" | "ERROR", text: string):
80
81
  .replace(/([\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}]\uFE0F?)\s{2,}/gu, "$1 ");
81
82
  if (!line) continue;
82
83
 
83
- // Prevent identical duplicate logs within the same second window
84
+ // Prevent identical duplicate logs within the same second or 1.5s window
85
+ const now = Date.now();
84
86
  const isDuplicate = logHistory
85
87
  .slice(-10)
86
- .some((entry) => entry.time === time && entry.level === level && entry.message === line);
88
+ .some(
89
+ (entry) =>
90
+ (entry.time === time || (entry._ts && now - entry._ts < 1500)) &&
91
+ entry.level === level &&
92
+ entry.message === line,
93
+ );
87
94
  if (isDuplicate) {
88
95
  continue;
89
96
  }
90
97
 
91
- const entry: ServerLogMessage = { time, level, message: line };
98
+ const entry: ServerLogMessage = { time, level, message: line, _ts: now };
92
99
  logHistory.push(entry);
93
100
  if (logHistory.length > MAX_LOG_HISTORY) {
94
101
  logHistory.shift();
@@ -17,6 +17,6 @@ export function getAppVersion(): string {
17
17
  }
18
18
  }
19
19
  } catch {}
20
- cachedVersion = "v1.4.8";
20
+ cachedVersion = "v1.5.0";
21
21
  return cachedVersion;
22
22
  }
@@ -1681,15 +1681,27 @@ async function tryCreateStreamWithRetry(
1681
1681
  }
1682
1682
 
1683
1683
  if (policy.reason === "account_initialization_failed") {
1684
- console.warn(
1685
- `⚠️ [Chat] Account initialization failed | ${currentAccountEmail} | cooldown=${Math.round((policy.accountCooldownMs ?? 0) / 1000)}s`,
1686
- );
1687
- markAccountFailed(currentAccountId);
1688
- markAccountRateLimited(
1689
- currentAccountId,
1690
- policy.accountCooldownMs,
1691
- policy.accountCooldownReason,
1692
- );
1684
+ const cooldownMs = policy.accountCooldownMs ?? 0;
1685
+ if (cooldownMs > 0) {
1686
+ console.warn(
1687
+ `⚠️ [Chat] Account initialization failed | ${currentAccountEmail} | cooldown=${Math.round(cooldownMs / 1000)}s`,
1688
+ );
1689
+ markAccountFailed(currentAccountId);
1690
+ markAccountRateLimited(
1691
+ currentAccountId,
1692
+ cooldownMs,
1693
+ policy.accountCooldownReason,
1694
+ );
1695
+ } else {
1696
+ console.warn(
1697
+ `⚠️ [Chat] Account auth expired/retryable | ${currentAccountEmail} | rotating without cooldown`,
1698
+ );
1699
+ if (policy.accountCooldownReason === "AuthExpired") {
1700
+ import("../../services/playwright.ts")
1701
+ .then(({ refreshAccountToken }) => refreshAccountToken(currentAccountId))
1702
+ .catch(() => {});
1703
+ }
1704
+ }
1693
1705
  return { success: false, error: err };
1694
1706
  }
1695
1707
 
@@ -1712,7 +1724,7 @@ async function tryCreateStreamWithRetry(
1712
1724
  console.warn(
1713
1725
  `🔄 [Chat] Switching account after ${policy.reason} | ${currentAccountEmail} -> ${maskEmail(nextAccount.email)}`,
1714
1726
  );
1715
- if (policy.accountCooldownMs || policy.accountCooldownReason) {
1727
+ if ((policy.accountCooldownMs ?? 0) > 0) {
1716
1728
  markAccountRateLimited(
1717
1729
  currentAccountId,
1718
1730
  policy.accountCooldownMs,
@@ -113,13 +113,41 @@ export function formatThinkingSummaryContent(delta: any): string {
113
113
  return sections.join("\n\n");
114
114
  }
115
115
 
116
+ /**
117
+ * Verified answer/content phases from Qwen Web:
118
+ * - "answer": standard text completions
119
+ * - "deep_research_answer", "ReportGeneration", "PdfMdGen": deep research & search modes
120
+ * - "image", "image_generation", "image_gen", "t2i": multimodal/image phases
121
+ */
122
+ export const ANSWER_PHASES = new Set([
123
+ "answer",
124
+ "deep_research_answer",
125
+ "ReportGeneration",
126
+ "PdfMdGen",
127
+ "image",
128
+ "image_generation",
129
+ "image_gen",
130
+ "t2i",
131
+ ]);
132
+
133
+ export function isAnswerPhase(phase: unknown): boolean {
134
+ return typeof phase === "string" && ANSWER_PHASES.has(phase);
135
+ }
136
+
116
137
  /**
117
138
  * Matches thinking phases verified in upstream Qwen Web HAR:
118
139
  * - "thinking_summary": structured thinking in Qwen Max / Plus
119
140
  * - "think": incremental thinking deltas in Qwen Omni
141
+ * - "DeepThinking": deep thinking models
120
142
  */
143
+ export const THINKING_PHASES = new Set([
144
+ "think",
145
+ "thinking_summary",
146
+ "DeepThinking",
147
+ ]);
148
+
121
149
  export function isThinkingPhase(phase: unknown): boolean {
122
- return phase === "think" || phase === "thinking_summary";
150
+ return typeof phase === "string" && THINKING_PHASES.has(phase);
123
151
  }
124
152
 
125
153
  /**
@@ -486,14 +486,14 @@ export function classifyRetryAction(
486
486
  switchAccount: true,
487
487
  forceNewChat: true,
488
488
  retryAfterMs: baseDelayMs,
489
- accountCooldownMs: config.concurrency.initFailureCooldownMs,
489
+ accountCooldownMs: 0,
490
490
  accountCooldownReason: "PersonalizationFailed",
491
491
  });
492
492
  }
493
493
 
494
494
  // Upstream 401 / Unauthorized on chat creation or API requests:
495
- // Account session is invalid or expired. Cool down account with AuthInitFailed so the
496
- // proxy rotates to a valid account instead of looping endlessly on 503s.
495
+ // Access token expired (15m window). Rotate without locking into a 300s cooldown
496
+ // so the account can immediately be renewed via refresh_token.
497
497
  if (
498
498
  code === "createchatinvalidresponse" ||
499
499
  code === "createchatfailed" ||
@@ -510,8 +510,8 @@ export function classifyRetryAction(
510
510
  forceNewChat: true,
511
511
  retryWithFullPrompt: true,
512
512
  retryAfterMs: Math.min(baseDelayMs, 1_000),
513
- accountCooldownMs: config.concurrency.initFailureCooldownMs,
514
- accountCooldownReason: "AuthInitFailed",
513
+ accountCooldownMs: 0,
514
+ accountCooldownReason: "AuthExpired",
515
515
  });
516
516
  }
517
517
  // Specialized recoveries first (even if wrapped as RetryableQwenStreamError)
@@ -68,6 +68,7 @@ import {
68
68
  import {
69
69
  getIncrementalDelta,
70
70
  isThinkingPhase,
71
+ isAnswerPhase,
71
72
  extractThinkingContent,
72
73
  formatThinkingSummaryContent,
73
74
  shouldSuppressStreamAbort,
@@ -470,7 +471,7 @@ export async function processNonStreamingResponse(
470
471
  foundStr = true;
471
472
  }
472
473
  }
473
- } else if (delta.phase === "answer") {
474
+ } else if (isAnswerPhase(delta.phase)) {
474
475
  isThinkingChunk = false;
475
476
  if (delta.content !== undefined) {
476
477
  const newContent = delta.content || "";
@@ -1436,7 +1437,7 @@ export async function processStreamingResponse(
1436
1437
 
1437
1438
  if (
1438
1439
  switchAccount &&
1439
- (policy.accountCooldownMs || policy.accountCooldownReason)
1440
+ (policy.accountCooldownMs ?? 0) > 0
1440
1441
  ) {
1441
1442
  markAccountRateLimited(
1442
1443
  currentAccountId,
@@ -1445,6 +1446,12 @@ export async function processStreamingResponse(
1445
1446
  );
1446
1447
  }
1447
1448
 
1449
+ if (policy.accountCooldownReason === "AuthExpired") {
1450
+ import("../../services/playwright.ts")
1451
+ .then(({ refreshAccountToken }) => refreshAccountToken(currentAccountId))
1452
+ .catch(() => {});
1453
+ }
1454
+
1448
1455
  retryContext.releaseAccountLease?.();
1449
1456
  retryContext.releaseAccountLease = null;
1450
1457
  await reader.cancel().catch(() => undefined);
@@ -1776,7 +1783,7 @@ export async function processStreamingResponse(
1776
1783
  // "phase":"answer"} delta and NO trailing [DONE]. Treat it as
1777
1784
  // the terminal event so we don't wait on the keep-alive
1778
1785
  // connection to close (up to the 60s/10min idle timeout).
1779
- if (delta.phase === "answer" && delta.status === "finished") {
1786
+ if (isAnswerPhase(delta.phase) && delta.status === "finished") {
1780
1787
  upstreamDone = true;
1781
1788
  if (!clientDisconnected) flushWrites();
1782
1789
  break; // Exit the for loop; the while check leaves the read loop
@@ -1800,7 +1807,7 @@ export async function processStreamingResponse(
1800
1807
  foundStr = true;
1801
1808
  }
1802
1809
  }
1803
- } else if (delta.phase === "answer") {
1810
+ } else if (isAnswerPhase(delta.phase)) {
1804
1811
  isThinkingChunk = false;
1805
1812
  if (delta.content !== undefined) {
1806
1813
  const newContent = delta.content || "";
@@ -2290,7 +2297,7 @@ export async function processStreamingResponse(
2290
2297
  // The retry stream may also terminate with an answer-finished
2291
2298
  // delta when upstream sends no [DONE]. Leave the read loop
2292
2299
  // immediately so we don't stall on the keep-alive connection.
2293
- if (delta.phase === "answer" && delta.status === "finished") {
2300
+ if (isAnswerPhase(delta.phase) && delta.status === "finished") {
2294
2301
  upstreamDone = true;
2295
2302
  break retryReadLoop;
2296
2303
  }
@@ -387,15 +387,18 @@ async function getSTSToken(
387
387
  }
388
388
  data = await response.json().catch(() => null);
389
389
  }
390
+ const codeLower = (data?.data?.code || data?.code || "").toLowerCase();
391
+ const detailsLower = (typeof data?.data?.details === "string" ? data.data.details : typeof data?.message === "string" ? data.message : "").toLowerCase();
390
392
  const is401 =
391
393
  data?.success === false &&
392
- (data?.data?.code === "Unauthorized" ||
393
- (typeof data?.data?.details === "string" &&
394
- data.data.details.includes("401")));
394
+ (codeLower === "unauthorized" ||
395
+ detailsLower.includes("401") ||
396
+ detailsLower.includes("token has expired") ||
397
+ detailsLower.includes("unauthorized"));
395
398
 
396
399
  if (is401) {
397
400
  try {
398
- const { refreshHeaders } = await import("../services/playwright.ts");
401
+ const { refreshAccountToken } = await import("../services/playwright.ts");
399
402
  const { getBasicHeaders } = await import("../services/auth-playwright.ts");
400
403
  const { loadAccounts } = await import("../core/accounts.ts");
401
404
  const resolvedId = accountId ?? loadAccounts()[0]?.id;
@@ -403,16 +406,18 @@ async function getSTSToken(
403
406
  console.warn(
404
407
  `[Upload] STS token 401 — refreshing session with re-auth and retrying...`,
405
408
  );
406
- await refreshHeaders(resolvedId, undefined, true);
407
- const fresh = await getBasicHeaders(resolvedId);
408
- headers.cookie = fresh.cookie;
409
- headers["user-agent"] = fresh.userAgent;
410
- headers["bx-v"] = fresh.bxV;
411
- if (fresh.bxUa) headers["bx-ua"] = fresh.bxUa;
412
- if (fresh.bxUmidtoken) headers["bx-umidtoken"] = fresh.bxUmidtoken;
413
- const retryRes = await doFetch(headers);
414
- if (retryRes.ok) {
415
- data = await retryRes.json().catch(() => null);
409
+ const refreshResult = await refreshAccountToken(resolvedId);
410
+ if (refreshResult.success) {
411
+ const fresh = await getBasicHeaders(resolvedId);
412
+ headers.cookie = fresh.cookie;
413
+ headers["user-agent"] = fresh.userAgent;
414
+ headers["bx-v"] = fresh.bxV;
415
+ if (fresh.bxUa) headers["bx-ua"] = fresh.bxUa;
416
+ if (fresh.bxUmidtoken) headers["bx-umidtoken"] = fresh.bxUmidtoken;
417
+ const retryRes = await doFetch(headers);
418
+ if (retryRes.ok) {
419
+ data = await retryRes.json().catch(() => null);
420
+ }
416
421
  }
417
422
  }
418
423
  } catch (refreshErr) {
@@ -71,18 +71,22 @@ async function createQwenChatSession(
71
71
  } catch {}
72
72
  }
73
73
 
74
- if (
75
- json?.data?.code === "Unauthorized" ||
76
- json?.code === "Unauthorized" ||
74
+ const isUnauthorized =
75
+ json?.data?.code?.toLowerCase() === "unauthorized" ||
76
+ json?.code?.toLowerCase() === "unauthorized" ||
77
77
  rawLower.includes("unauthorized") ||
78
- rawLower.includes("permission to access")
79
- ) {
78
+ rawLower.includes("permission to access") ||
79
+ rawLower.includes("token has expired") ||
80
+ rawLower.includes("401");
81
+
82
+ if (isUnauthorized) {
80
83
  if (accountId) {
81
84
  try {
82
85
  const { refreshAccountToken } = await import("./playwright.ts");
83
86
  const refreshed = await refreshAccountToken(accountId);
84
87
  if (refreshed.success) {
85
- return createQwenChatSession(headers, model, accountId, chatMode);
88
+ const { headers: freshHeaders } = await getQwenHeaders(false, accountId);
89
+ return createQwenChatSession(freshHeaders, model, accountId, chatMode);
86
90
  }
87
91
  } catch {}
88
92
  }
@@ -79,7 +79,8 @@ export function buildQwenRequestHeaders(
79
79
  "bx-v": opts.bxV || "2.5.37",
80
80
  source: "web",
81
81
  version: opts.version || getQwenWebVersion(),
82
- timezone: opts.extra?.timezone || new Date().toString().split(" (")[0],
82
+ timezone: opts.extra?.timezone || opts.extra?.Timezone || new Date().toString().split(" (")[0],
83
+ Timezone: opts.extra?.Timezone || opts.extra?.timezone || new Date().toString().split(" (")[0],
83
84
  // Use the real browser client-hints when captured (anti-hardcoded); fall
84
85
  // back to the static fingerprint otherwise.
85
86
  "sec-ch-ua":
@@ -2763,10 +2763,19 @@ async function createQwenStreamInternal(
2763
2763
  };
2764
2764
 
2765
2765
  // A new logical chat session should reuse the warmed header cache when available.
2766
- // Header recapture is much more expensive and should be reserved for real refresh/login cases,
2767
- // not for ordinary first prompts that simply need parent_id reset.
2766
+ // Proactively check if the access token is expiring soon (<3m) and refresh quietly before starting.
2767
+ let forceRefresh = options?.forceNewChat === true;
2768
+ if (!forceRefresh && accountId) {
2769
+ try {
2770
+ const basic = await getBasicHeaders(accountId);
2771
+ if (isTokenExpiringSoon(basic.cookie, 3)) {
2772
+ forceRefresh = true;
2773
+ }
2774
+ } catch {}
2775
+ }
2776
+
2768
2777
  const captured = await getQwenHeaders(
2769
- options?.forceNewChat === true,
2778
+ forceRefresh,
2770
2779
  accountId,
2771
2780
  );
2772
2781
  ensureNotAborted();
@@ -2782,7 +2791,7 @@ async function createQwenStreamInternal(
2782
2791
  if (options && "chatSessionId" in options) {
2783
2792
  if (options.chatSessionId === null || options.chatSessionId === "") {
2784
2793
  const acquired = await acquireNewQwenChatSession(
2785
- headers,
2794
+ activeHeaders,
2786
2795
  model,
2787
2796
  accountId,
2788
2797
  options?.chatMode ?? "thread",
@@ -2790,6 +2799,10 @@ async function createQwenStreamInternal(
2790
2799
  chatSessionId = acquired.chatId;
2791
2800
  leasedWarmChat = acquired.leasedFromPool;
2792
2801
  createdNewChat = true;
2802
+ try {
2803
+ const fresh = await getQwenHeaders(false, accountId);
2804
+ activeHeaders = fresh.headers;
2805
+ } catch {}
2793
2806
  } else {
2794
2807
  chatSessionId = options.chatSessionId;
2795
2808
  }
@@ -2797,7 +2810,7 @@ async function createQwenStreamInternal(
2797
2810
  chatSessionId = captured.chatSessionId;
2798
2811
  if (!chatSessionId) {
2799
2812
  const acquired = await acquireNewQwenChatSession(
2800
- headers,
2813
+ activeHeaders,
2801
2814
  model,
2802
2815
  accountId,
2803
2816
  options?.chatMode ?? "thread",
@@ -2805,6 +2818,10 @@ async function createQwenStreamInternal(
2805
2818
  chatSessionId = acquired.chatId;
2806
2819
  leasedWarmChat = acquired.leasedFromPool;
2807
2820
  createdNewChat = true;
2821
+ try {
2822
+ const fresh = await getQwenHeaders(false, accountId);
2823
+ activeHeaders = fresh.headers;
2824
+ } catch {}
2808
2825
  }
2809
2826
  }
2810
2827