qwenproxy-cli 1.3.5 → 1.4.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.
@@ -9,6 +9,7 @@ import {
9
9
  import { mapClientModelToQwen } from "../core/model-alias.ts";
10
10
  import { qwenUrl } from "./qwen-url.ts";
11
11
  import { QwenUpstreamError } from "./qwen-errors.ts";
12
+ import { looksLikeAntiBotChallengeText } from "./media-generation.ts";
12
13
  import {
13
14
  requestQwenTextInBrowser,
14
15
  buildCapturedQwenHeaders,
@@ -57,6 +58,31 @@ async function createQwenChatSession(
57
58
  json?.data?.chat?.id;
58
59
 
59
60
  if (!chatId || typeof chatId !== "string") {
61
+ const rawLower = raw.toLowerCase();
62
+ if (looksLikeAntiBotChallengeText(raw)) {
63
+ try {
64
+ const { recoverBaxiaCaptcha } = await import("./captcha-coordinator.ts");
65
+ const solved = await recoverBaxiaCaptcha(accountId, "createQwenChatSession", {
66
+ challengeBody: raw,
67
+ });
68
+ if (solved) {
69
+ return createQwenChatSession(headers, model, accountId, chatMode);
70
+ }
71
+ } catch {}
72
+ }
73
+
74
+ if (
75
+ json?.data?.code === "Unauthorized" ||
76
+ json?.code === "Unauthorized" ||
77
+ rawLower.includes("unauthorized") ||
78
+ rawLower.includes("permission to access")
79
+ ) {
80
+ throw new QwenUpstreamError(
81
+ `Qwen create chat unauthorized: ${raw.substring(0, 300)}`,
82
+ "Unauthorized",
83
+ 401,
84
+ );
85
+ }
60
86
  throw new QwenUpstreamError(
61
87
  `Qwen create chat returned unexpected payload: ${raw.substring(0, 300)}`,
62
88
  "CreateChatInvalidResponse",
@@ -89,6 +89,14 @@ export function buildQwenRequestHeaders(
89
89
  "sec-ch-ua-platform": opts.secChUaPlatform || '"Windows"',
90
90
  };
91
91
 
92
+ const tokenMatch = opts.cookie?.match(/(?:^|;\s*)token=([^;]+)/);
93
+ if (tokenMatch && !headers["Authorization"] && !headers["authorization"]) {
94
+ const bearer = decodeURIComponent(tokenMatch[1].trim());
95
+ if (bearer && bearer !== '""' && bearer !== "null") {
96
+ headers["Authorization"] = `Bearer ${bearer}`;
97
+ }
98
+ }
99
+
92
100
  // The real chat.qwen.ai client sends ONLY bx-v on API requests — the WAF
93
101
  // carries bx-ua/bx-umidtoken as browser cookies, not headers. Match that
94
102
  // unless QWEN_SEND_BX_UA=true restores the legacy injection.
@@ -663,8 +663,18 @@ const QWEN_SAFE_SETTINGS_PATCH = {
663
663
  autoTags: false,
664
664
  largeTextAsFile: false,
665
665
  splitLargeChunks: false,
666
+ title: {
667
+ auto: false,
668
+ },
669
+ notificationEnabled: false,
666
670
  },
667
671
  mcp_remind: false,
672
+ mcp: {
673
+ "code-interpreter": false,
674
+ "fire-crawl": false,
675
+ amap: false,
676
+ "image-generation": false,
677
+ },
668
678
  memory: {
669
679
  enable_memory: false,
670
680
  enable_history_memory: false,
@@ -680,6 +690,10 @@ const QWEN_SAFE_SETTINGS_PATCH = {
680
690
  image_edit_tool: false,
681
691
  bio: false,
682
692
  image_zoom_in_tool: false,
693
+ image_search: false,
694
+ },
695
+ extension: {
696
+ show_guide: false,
683
697
  },
684
698
  } as const;
685
699
 
@@ -813,6 +827,7 @@ function getBrowserFetchHeaders(
813
827
  const browserAllowedHeaders = new Set([
814
828
  "accept",
815
829
  "content-type",
830
+ "authorization",
816
831
  "bx-ua",
817
832
  "bx-umidtoken",
818
833
  "bx-v",
@@ -1573,6 +1588,7 @@ export async function syncQwenRequestPersonalization(
1573
1588
  existingJson?.data?.ui?.largeTextAsFile === false &&
1574
1589
  existingJson?.data?.ui?.splitLargeChunks === false &&
1575
1590
  existingJson?.data?.ui?.autoTags === false &&
1591
+ existingJson?.data?.ui?.title?.auto === false &&
1576
1592
  existingJson?.data?.mcp_remind === false &&
1577
1593
  existingJson?.data?.memory?.enable_memory === false &&
1578
1594
  existingJson?.data?.memory?.enable_history_memory === false &&
@@ -1669,6 +1685,7 @@ export async function syncQwenRequestPersonalization(
1669
1685
  `[Qwen] Personalization 401 — refreshing session with re-auth and retrying | account=${cacheKey}`,
1670
1686
  );
1671
1687
  try {
1688
+ currentSettings = null;
1672
1689
  const { headers: freshHeaders } = await getQwenHeaders(true, accountId, true);
1673
1690
  requestHeaders = buildCapturedQwenHeaders(freshHeaders, {
1674
1691
  referer: qwenUrl("/settings/personalization"),
@@ -1977,53 +1994,104 @@ function formatPublicQwenModel(model: Record<string, unknown>): PublicQwenModel
1977
1994
  }
1978
1995
 
1979
1996
  export async function deleteAllQwenChats(accountId?: string): Promise<boolean> {
1980
- let requestHeaders: Record<string, string>;
1981
1997
  if (isAuthMockEnabled()) {
1982
1998
  const { headers } = await getQwenHeaders(false, accountId);
1983
- requestHeaders = buildCapturedQwenHeaders(headers, {
1999
+ const requestHeaders = buildCapturedQwenHeaders(headers, {
1984
2000
  referer: qwenUrl("/settings/chats"),
1985
2001
  });
1986
- } else {
1987
- // In live mode, requestQwenTextInBrowser executes inside the authenticated
1988
- // browser page where session cookies are attached automatically.
1989
- // Bypassing getQwenHeaders avoids triggering captureQwenHeaders (which sends
1990
- // a dummy chat completion to intercept anti-fraud tokens not needed for deletions).
1991
- requestHeaders = {
1992
- source: "web",
1993
- version: "0.2.89",
1994
- timezone: new Date().toString().split(" (")[0],
1995
- "x-request-id": crypto.randomUUID(),
1996
- Referer: qwenUrl("/settings/chats"),
1997
- };
1998
- }
1999
-
2000
- const response = await requestQwenTextInBrowser(
2001
- accountId,
2002
- "DELETE",
2003
- "/api/v2/chats/",
2004
- requestHeaders,
2005
- undefined,
2006
- { referrer: qwenUrl("/settings/chats") },
2007
- );
2008
2002
 
2009
- const { raw, json: parsed } = await readJsonTextResponse(response, {
2010
- strict: true,
2011
- });
2012
- if (!response.ok) {
2013
- throw new Error(
2014
- `Failed to delete chats from Qwen: ${response.status} ${raw.substring(0, 200)}`,
2003
+ const response = await requestQwenTextInBrowser(
2004
+ accountId,
2005
+ "DELETE",
2006
+ "/api/v2/chats/",
2007
+ requestHeaders,
2008
+ undefined,
2009
+ { referrer: qwenUrl("/settings/chats") },
2015
2010
  );
2011
+
2012
+ const { raw, json: parsed } = await readJsonTextResponse(response, {
2013
+ strict: true,
2014
+ });
2015
+ if (!response.ok) {
2016
+ throw new Error(
2017
+ `Failed to delete chats from Qwen: ${response.status} ${raw.substring(0, 200)}`,
2018
+ );
2019
+ }
2020
+
2021
+ const success = parsed?.success === true && parsed?.data?.status === true;
2022
+ if (!success) {
2023
+ throw new Error(
2024
+ `Qwen delete chats returned unexpected payload: ${raw.substring(0, 200)}`,
2025
+ );
2026
+ }
2027
+
2028
+ clearAllSessionsForAccount(accountId || "global");
2029
+ return true;
2016
2030
  }
2017
2031
 
2018
- const success = parsed?.success === true && parsed?.data?.status === true;
2019
- if (!success) {
2020
- throw new Error(
2021
- `Qwen delete chats returned unexpected payload: ${raw.substring(0, 200)}`,
2032
+ // Live mode: fetch remote chats and delete them by ID in parallel batches
2033
+ // (Alibaba WAF rejects bare DELETE /api/v2/chats/ with 401; deleting by ID succeeds 100%).
2034
+ try {
2035
+ const { withAccountPage } = await import("./playwright.ts");
2036
+ const result = await withAccountPage(
2037
+ accountId || "global",
2038
+ async (page) => {
2039
+ return page.evaluate(async () => {
2040
+ let totalDeleted = 0;
2041
+ let pageNum = 1;
2042
+ while (true) {
2043
+ const listRes = await fetch(`/api/v2/chats/?page=${pageNum}&exclude_project=true`, {
2044
+ credentials: "include",
2045
+ headers: { accept: "application/json, text/plain, */*" },
2046
+ });
2047
+ const listJson = await listRes.json().catch(() => null);
2048
+ const items = listJson?.data || [];
2049
+ if (!Array.isArray(items) || items.length === 0) break;
2050
+
2051
+ for (let i = 0; i < items.length; i += 5) {
2052
+ const batch = items.slice(i, i + 5);
2053
+ await Promise.all(
2054
+ batch.map(async (chat: any) => {
2055
+ try {
2056
+ await fetch(`/api/v2/chats/${chat.id}`, {
2057
+ method: "DELETE",
2058
+ credentials: "include",
2059
+ headers: { accept: "application/json, text/plain, */*" },
2060
+ });
2061
+ totalDeleted++;
2062
+ } catch {}
2063
+ }),
2064
+ );
2065
+ }
2066
+
2067
+ if (items.length < 20) break;
2068
+ }
2069
+ return { success: true, count: totalDeleted };
2070
+ });
2071
+ },
2072
+ 60_000,
2073
+ 30_000,
2074
+ true,
2022
2075
  );
2023
- }
2024
2076
 
2025
- clearAllSessionsForAccount(accountId || "global");
2026
- return true;
2077
+ clearAllSessionsForAccount(accountId || "global");
2078
+ return result?.success === true;
2079
+ } catch (err: any) {
2080
+ try {
2081
+ const chats = await fetchRemoteQwenChats(accountId);
2082
+ if (chats.length === 0) {
2083
+ clearAllSessionsForAccount(accountId || "global");
2084
+ return true;
2085
+ }
2086
+ for (const chat of chats) {
2087
+ await deleteSingleQwenChat(accountId, chat.id).catch(() => false);
2088
+ }
2089
+ clearAllSessionsForAccount(accountId || "global");
2090
+ return true;
2091
+ } catch {
2092
+ return false;
2093
+ }
2094
+ }
2027
2095
  }
2028
2096
 
2029
2097
  export interface RemoteQwenChat {
@@ -412,7 +412,13 @@ function findPartialToolOpenIndexOutsideMarkdownCode(
412
412
  if (!tailLower.includes(">")) {
413
413
  for (const name of openNames) {
414
414
  const full = `<${name.toLowerCase()}`;
415
- if (full.startsWith(tailLower)) {
415
+ if (
416
+ full.startsWith(tailLower) ||
417
+ tailLower.startsWith(full + "=") ||
418
+ tailLower.startsWith(full + " ") ||
419
+ tailLower.startsWith(full + "\t") ||
420
+ tailLower.startsWith(full + ":")
421
+ ) {
416
422
  return i;
417
423
  }
418
424
  }
package/src/tui/app.ts CHANGED
@@ -7,19 +7,9 @@ import type { TuiView, ProxyStatusSnapshot } from "./types.ts";
7
7
  import { theme, glyphs, drawBox, stringWidth } from "./theme.ts";
8
8
  import { fetchProxyStatus, fetchLiveModels, getCachedLiveModels } from "./proxy-client.ts";
9
9
  import { ServerManager } from "./server-manager.ts";
10
- import fs from "node:fs";
11
- import path from "node:path";
12
- import { fileURLToPath } from "node:url";
10
+ import { getAppVersion } from "../core/version.ts";
13
11
 
14
- let cachedAppVersion = "";
15
- try {
16
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
17
- const pkgPath = path.resolve(currentDir, "../../package.json");
18
- if (fs.existsSync(pkgPath)) {
19
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
20
- if (pkg.version) cachedAppVersion = `v${pkg.version}`;
21
- }
22
- } catch {}
12
+ const cachedAppVersion = getAppVersion();
23
13
  import { StatusView } from "./views/status-view.ts";
24
14
  import { ChatView } from "./views/chat-view.ts";
25
15
  import { SyncView } from "./views/sync-view.ts";
@@ -247,9 +247,11 @@ export class ServerManager {
247
247
 
248
248
  // 2. Start in-process
249
249
  this.state = "warming";
250
+ const { getAppVersion } = await import("../core/version.ts");
251
+ const appVersion = getAppVersion();
250
252
  this.appendLog(
251
253
  "INFO",
252
- `🚀 [Server] Iniciando servidor na porta ${port}...`,
254
+ `🚀 [Server] Iniciando QwenProxy ${appVersion} na porta ${port}...`,
253
255
  );
254
256
 
255
257
  this.interceptLogs();