qwenproxy-cli 1.0.4 → 1.0.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/README.md +2 -2
- package/bin/qwenproxy.js +41 -13
- package/package.json +1 -1
- package/src/api/server.ts +4 -1
- package/src/clean-cache.ts +17 -11
- package/src/core/config.ts +2 -0
- package/src/core/model-alias.ts +47 -7
- package/src/login.ts +5 -0
- package/src/routes/chat/validation.ts +1 -1
- package/src/services/fingerprint.ts +45 -5
- package/src/services/playwright.ts +120 -8
- package/src/services/qwen-chat-pool.ts +4 -4
- package/src/services/qwen-headers.ts +28 -6
- package/src/sync/omp.ts +27 -21
- package/src/sync/opencode.ts +34 -30
- package/src/tui/app.ts +1 -1
- package/src/tui/proxy-client.ts +50 -49
- package/src/tui/screen.ts +47 -7
- package/src/tui/server-manager.ts +19 -11
- package/src/tui/theme.ts +41 -23
- package/src/tui/views/accounts-view.ts +5 -3
- package/src/tui/views/chat-view.ts +1 -0
- package/src/tui/views/status-view.ts +4 -4
- package/src/tui/views/storage-view.ts +11 -7
- package/src/update-cli.ts +21 -6
package/README.md
CHANGED
|
@@ -284,7 +284,7 @@ qpx
|
|
|
284
284
|
| `qpx login` | Abre navegador visível para autenticar novas contas interativamente |
|
|
285
285
|
| `qpx sync` | Configura e sincroniza clientes (Claude Code, Codex, OpenCode, OMP) |
|
|
286
286
|
| `qpx clean` | Limpa caches temporários dos perfis Chromium (~4.5MB por conta) |
|
|
287
|
-
| `qpx clean:all` | Limpa caches e remove versões antigas de navegadores órfãos
|
|
287
|
+
| `qpx clean:all` | Limpa caches e remove versões antigas de navegadores órfãos em disco |
|
|
288
288
|
| `qpx purge` | Limpa o histórico de conversas remotas no Qwen de todas as contas |
|
|
289
289
|
| `qpx reset` | Reseta cooldowns e rate limits salvos no banco de dados |
|
|
290
290
|
### Opção 2: Execução Instantânea (Zero Instalação)
|
|
@@ -845,7 +845,7 @@ QwenProxy/
|
|
|
845
845
|
| `npm start` | Iniciar apenas o servidor QwenProxy em modo headless |
|
|
846
846
|
| `npm run sync` | Sincronizar clientes (Claude Code, Codex, OpenCode, OMP) com backup |
|
|
847
847
|
| `npm run clean` | Limpar caches temporários dos perfis Chromium (~4.5MB por conta) |
|
|
848
|
-
| `npm run clean:all` | Limpar caches + remover navegadores órfãos e versões antigas
|
|
848
|
+
| `npm run clean:all` | Limpar caches + remover navegadores órfãos e versões antigas em disco |
|
|
849
849
|
| `npm run reset` | Zerar cooldowns de contas no banco de dados |
|
|
850
850
|
| `npm run login` | Adicionar/autenticar novas contas visualmente no navegador |
|
|
851
851
|
| `npm run purge` | Limpar chats remotos do Qwen nas contas configuradas |
|
package/bin/qwenproxy.js
CHANGED
|
@@ -115,19 +115,47 @@ try {
|
|
|
115
115
|
tsxLoaderArg = pathToFileURL(tsxEntry).href;
|
|
116
116
|
} catch {}
|
|
117
117
|
|
|
118
|
-
// Ensure Playwright Chromium is installed for
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
118
|
+
// Ensure Playwright Chromium is installed only for commands that need the browser
|
|
119
|
+
const browserCommands = ["start", "tui", "login"];
|
|
120
|
+
const isBrowserCommand =
|
|
121
|
+
!firstArg ||
|
|
122
|
+
browserCommands.includes(firstArg) ||
|
|
123
|
+
rawArgs.includes("--tui") ||
|
|
124
|
+
rawArgs.includes("--server");
|
|
125
|
+
|
|
126
|
+
if (isBrowserCommand) {
|
|
127
|
+
try {
|
|
128
|
+
const { chromium } = await import("patchright");
|
|
129
|
+
const execPath = chromium.executablePath();
|
|
130
|
+
if (!fs.existsSync(execPath)) {
|
|
131
|
+
console.log("⏳ [QwenProxy] Instalando o navegador Chromium pela primeira vez...");
|
|
132
|
+
let cliPath = "";
|
|
133
|
+
try {
|
|
134
|
+
const patchrightEntry = require.resolve("patchright");
|
|
135
|
+
cliPath = path.join(path.dirname(patchrightEntry), "cli.js");
|
|
136
|
+
} catch {}
|
|
137
|
+
const installEnv = {
|
|
138
|
+
...process.env,
|
|
139
|
+
PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT:
|
|
140
|
+
process.env.PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT || "300000",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (cliPath && fs.existsSync(cliPath)) {
|
|
144
|
+
spawnSync(process.execPath, [cliPath, "install", "chromium"], {
|
|
145
|
+
stdio: "inherit",
|
|
146
|
+
env: installEnv,
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
const cmd = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
150
|
+
spawnSync(cmd, ["--yes", "patchright", "install", "chromium"], {
|
|
151
|
+
stdio: "inherit",
|
|
152
|
+
env: installEnv,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
console.log("✓ [QwenProxy] Navegador instalado com sucesso!\n");
|
|
156
|
+
}
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
131
159
|
|
|
132
160
|
const child = spawn(process.execPath, ["--import", tsxLoaderArg, targetPath, ...scriptArgs], {
|
|
133
161
|
stdio: "inherit",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
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": {
|
package/src/api/server.ts
CHANGED
|
@@ -597,6 +597,7 @@ export async function stopServer(): Promise<void> {
|
|
|
597
597
|
|
|
598
598
|
export async function startServer(options?: {
|
|
599
599
|
installSignalHandlers?: boolean;
|
|
600
|
+
showBanner?: boolean;
|
|
600
601
|
}): Promise<StartedServerInfo> {
|
|
601
602
|
if (server) {
|
|
602
603
|
if (options?.installSignalHandlers !== false) installSignalHandlers();
|
|
@@ -829,7 +830,8 @@ export async function startServer(options?: {
|
|
|
829
830
|
|
|
830
831
|
const endpoint = `${started.url}/v1`;
|
|
831
832
|
|
|
832
|
-
|
|
833
|
+
if (options?.showBanner !== false) {
|
|
834
|
+
console.log(`
|
|
833
835
|
+${"-".repeat(W)}+
|
|
834
836
|
|${blank()}|
|
|
835
837
|
|${center("QwenProxy")}|
|
|
@@ -845,6 +847,7 @@ export async function startServer(options?: {
|
|
|
845
847
|
|${blank()}|
|
|
846
848
|
+${"-".repeat(W)}+
|
|
847
849
|
`);
|
|
850
|
+
}
|
|
848
851
|
return started;
|
|
849
852
|
})();
|
|
850
853
|
|
package/src/clean-cache.ts
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import { chromium } from "patchright";
|
|
6
|
-
import { pruneAllPlaywrightProfiles } from "./services/playwright.ts";
|
|
6
|
+
import { pruneAllPlaywrightProfiles, cleanupOrphanProfiles } from "./services/playwright.ts";
|
|
7
7
|
|
|
8
8
|
export function formatBytes(bytes: number): string {
|
|
9
9
|
if (bytes < 1024) return `${bytes} B`;
|
|
@@ -141,13 +141,19 @@ async function main() {
|
|
|
141
141
|
// 1. Profile Transient Cache Pruning (V8 Code Cache, GPU Cache)
|
|
142
142
|
console.log("1. Limpando caches transitórios dos perfis (data/qwen_profiles/)...");
|
|
143
143
|
const profileResult = pruneAllPlaywrightProfiles();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
144
|
+
const orphanResult = cleanupOrphanProfiles();
|
|
145
|
+
if (profileResult.totalFreedFiles > 0 || orphanResult.removedCount > 0) {
|
|
146
|
+
console.log(` [OK] Perfis limpos com sucesso!`);
|
|
147
|
+
if (profileResult.totalFreedFiles > 0) {
|
|
148
|
+
console.log(
|
|
149
|
+
` Espaço liberado: ${formatBytes(profileResult.totalFreedBytes)} em ${profileResult.totalFreedFiles} arquivos (${profileResult.profilesCleaned} perfil(is)).`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (orphanResult.removedCount > 0) {
|
|
153
|
+
console.log(
|
|
154
|
+
` Perfis órfãos removidos: ${orphanResult.removedCount} (${formatBytes(orphanResult.freedBytes)} liberados).`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
151
157
|
console.log(
|
|
152
158
|
` (Todos os cookies, sessões e logins foram 100% preservados!)`,
|
|
153
159
|
);
|
|
@@ -172,13 +178,13 @@ async function main() {
|
|
|
172
178
|
for (const d of browserResult.unusedDirs) {
|
|
173
179
|
console.log(` - ${d.name} (${d.size})`);
|
|
174
180
|
}
|
|
175
|
-
console.log(` Total recuperado
|
|
181
|
+
console.log(` Total recuperado em disco: ${formatBytes(browserResult.freedBytes)}!`);
|
|
176
182
|
} else {
|
|
177
|
-
console.log(` [INFO] Encontrados ${browserResult.unusedDirs.length} navegador(es) legados/não utilizados
|
|
183
|
+
console.log(` [INFO] Encontrados ${browserResult.unusedDirs.length} navegador(es) legados/não utilizados em disco:`);
|
|
178
184
|
for (const d of browserResult.unusedDirs) {
|
|
179
185
|
console.log(` - ${d.name} (${d.size})`);
|
|
180
186
|
}
|
|
181
|
-
console.log(` Espaço recuperável
|
|
187
|
+
console.log(` Espaço recuperável em disco: ${formatBytes(totalReclaimable)}.`);
|
|
182
188
|
console.log(` Para liberar esse espaço automaticamente, execute:`);
|
|
183
189
|
console.log(` npm run clean:all\n`);
|
|
184
190
|
}
|
package/src/core/config.ts
CHANGED
|
@@ -154,6 +154,7 @@ const envSchema = z
|
|
|
154
154
|
QWEN_PERSONALIZATION_FROM_REQUEST: z.string().default("true"),
|
|
155
155
|
QWEN_PERSONALIZATION_VERIFY_GET: z.string().default("true"),
|
|
156
156
|
QWEN_BROWSER_ONLY_FETCH: z.string().default("true"),
|
|
157
|
+
QWEN_MAP_OPENAI_MODELS: z.string().default("true"),
|
|
157
158
|
QWEN_MAX_PROMPT_BYTES: z.string().default("0"),
|
|
158
159
|
QWEN_MAX_PERSONALIZATION_BYTES: z.string().default("200000"),
|
|
159
160
|
CONTEXT_METER_ENABLED: z.string().default("true"),
|
|
@@ -358,6 +359,7 @@ export const config = {
|
|
|
358
359
|
/** "thread" (reuse upstream chat) or "temp" (new ephemeral chat per request). */
|
|
359
360
|
/** When true, all requests (personalization, models, media, chat) route exclusively through the browser page (no direct Node fetch). */
|
|
360
361
|
browserOnlyFetch: env.QWEN_BROWSER_ONLY_FETCH !== "false",
|
|
362
|
+
mapOpenAiModels: env.QWEN_MAP_OPENAI_MODELS !== "false",
|
|
361
363
|
chatMode: env.QWEN_CHAT_MODE,
|
|
362
364
|
maxPromptBytes: Math.max(0, parseInt(env.QWEN_MAX_PROMPT_BYTES)),
|
|
363
365
|
maxPersonalizationBytes: Math.max(
|
package/src/core/model-alias.ts
CHANGED
|
@@ -63,15 +63,55 @@ export function stripThinkingSuffix(model: string): {
|
|
|
63
63
|
return { baseModel: normalizedModel, enableThinking: true, reasoningMode: "auto" };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Mapeia modelos populares de terceiros (OpenAI / Anthropic) para o tier Qwen equivalente:
|
|
68
|
+
* - mini / haiku / 3.5 -> qwen3.7-plus
|
|
69
|
+
* - gpt-4* / o1* / o3* / opus / sonnet / claude-* -> qwen3.8-max
|
|
70
|
+
* Modelos que já começam com "qwen" passam intactos.
|
|
71
|
+
*/
|
|
72
|
+
export function mapKnownModelAlias(model: string): string {
|
|
73
|
+
if (!model) return model;
|
|
74
|
+
const lower = model.toLowerCase();
|
|
75
|
+
if (lower.startsWith("qwen")) return model;
|
|
76
|
+
|
|
77
|
+
// Lightweight / mini models
|
|
78
|
+
if (
|
|
79
|
+
lower.includes("mini") ||
|
|
80
|
+
lower.includes("haiku") ||
|
|
81
|
+
lower.includes("3.5-turbo")
|
|
82
|
+
) {
|
|
83
|
+
return "qwen3.7-plus";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Flagship reasoning / chat models
|
|
87
|
+
if (
|
|
88
|
+
lower.startsWith("gpt-") ||
|
|
89
|
+
lower.startsWith("chatgpt-") ||
|
|
90
|
+
lower.startsWith("o1") ||
|
|
91
|
+
lower.startsWith("o3") ||
|
|
92
|
+
lower.includes("sonnet") ||
|
|
93
|
+
lower.includes("opus") ||
|
|
94
|
+
lower.startsWith("claude")
|
|
95
|
+
) {
|
|
96
|
+
return "qwen3.8-max";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return model;
|
|
100
|
+
}
|
|
101
|
+
|
|
66
102
|
/**
|
|
67
103
|
* Mapeia o id de modelo para o Qwen upstream.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* provider envia o id Qwen correto, e qualquer id desconhecido deve chegar ao
|
|
71
|
-
* upstream para que este responda um erro claro de modelo, em vez de ser
|
|
72
|
-
* silenciosamente roteado para um tier qualquer.
|
|
104
|
+
* Quando enableAliases é verdadeiro (padrão via QWEN_MAP_OPENAI_MODELS),
|
|
105
|
+
* converte gpt-* / o1* / claude-* para os tiers correspondentes do Qwen.
|
|
73
106
|
*/
|
|
74
|
-
export function mapClientModelToQwen(
|
|
107
|
+
export function mapClientModelToQwen(
|
|
108
|
+
model: string,
|
|
109
|
+
enableAliases = true,
|
|
110
|
+
): string {
|
|
75
111
|
if (!model) return model;
|
|
76
|
-
|
|
112
|
+
const base = stripThinkingSuffix(model.trim()).baseModel;
|
|
113
|
+
if (enableAliases) {
|
|
114
|
+
return mapKnownModelAlias(base);
|
|
115
|
+
}
|
|
116
|
+
return base;
|
|
77
117
|
}
|
package/src/login.ts
CHANGED
|
@@ -130,6 +130,11 @@ async function removeAccountFlow() {
|
|
|
130
130
|
const confirm = await askQuestion(`\nRemove ${account.email}? (y/N): `);
|
|
131
131
|
if (confirm.toLowerCase() === "y") {
|
|
132
132
|
if (removeAccount(account.id)) {
|
|
133
|
+
try {
|
|
134
|
+
const { removePlaywrightProfile } = await import("./services/playwright.ts");
|
|
135
|
+
const { getAccountProfilePath } = await import("./core/paths.ts");
|
|
136
|
+
removePlaywrightProfile(getAccountProfilePath(account.id));
|
|
137
|
+
} catch {}
|
|
133
138
|
console.log(`Account ${maskEmail(account.email)} removed.`);
|
|
134
139
|
} else {
|
|
135
140
|
console.log("Failed to remove account.");
|
|
@@ -70,7 +70,7 @@ export async function parseRequestBody(c: Context): Promise<ParsedRequest> {
|
|
|
70
70
|
|
|
71
71
|
// Thinking suffixes → base model + reasoning mode
|
|
72
72
|
const { baseModel, enableThinking, reasoningMode } = stripThinkingSuffix(body.model);
|
|
73
|
-
const modelId = mapClientModelToQwen(baseModel);
|
|
73
|
+
const modelId = mapClientModelToQwen(baseModel, config.qwen.mapOpenAiModels);
|
|
74
74
|
|
|
75
75
|
// OpenAI `reasoning_effort` (none|minimal|low|medium|high|xhigh|max).
|
|
76
76
|
// Precedence: an explicit model suffix wins — effort only acts on unsuffixed
|
|
@@ -23,9 +23,48 @@ function pick<T>(rng: () => number, values: readonly T[]): T {
|
|
|
23
23
|
function randInt(rng: () => number, min: number, max: number): number {
|
|
24
24
|
return Math.floor(rng() * (max - min + 1)) + min;
|
|
25
25
|
}
|
|
26
|
+
let dynamicChromeMajor = 151;
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
export function updateChromeMajor(major: number): void {
|
|
29
|
+
if (typeof major === "number" && major >= 100) {
|
|
30
|
+
dynamicChromeMajor = major;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getChromeMajor(): number {
|
|
35
|
+
return dynamicChromeMajor;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getSystemLocale(): string {
|
|
39
|
+
return (
|
|
40
|
+
process.env.QWEN_LOCALE ||
|
|
41
|
+
(typeof Intl !== "undefined" && Intl.DateTimeFormat
|
|
42
|
+
? Intl.DateTimeFormat().resolvedOptions().locale
|
|
43
|
+
: "") ||
|
|
44
|
+
"pt-BR"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getLanguageProfiles(locale = getSystemLocale()): readonly (readonly string[])[] {
|
|
49
|
+
const primary = locale.split("-")[0];
|
|
50
|
+
if (primary === "en") {
|
|
51
|
+
return [
|
|
52
|
+
["en-US", "en"],
|
|
53
|
+
["en-US", "en", "es"],
|
|
54
|
+
["en", "en-US"],
|
|
55
|
+
["en-US", "en;q=0.9"],
|
|
56
|
+
] as const;
|
|
57
|
+
}
|
|
58
|
+
return [
|
|
59
|
+
[locale, primary, "en-US", "en"],
|
|
60
|
+
[locale, primary, "en-US", "en", "es"],
|
|
61
|
+
[locale, "en-US", "en", primary],
|
|
62
|
+
[locale, primary, "en"],
|
|
63
|
+
[locale, `${primary};q=0.9`, "en-US;q=0.8", "en;q=0.7"],
|
|
64
|
+
] as const;
|
|
65
|
+
}
|
|
28
66
|
|
|
67
|
+
const CHROME_MAJOR = 151;
|
|
29
68
|
const VIEWPORTS = [
|
|
30
69
|
{ width: 1366, height: 768 },
|
|
31
70
|
{ width: 1440, height: 900 },
|
|
@@ -176,20 +215,21 @@ export function getFingerprintProfile(accountId: string): FingerprintProfile {
|
|
|
176
215
|
const rng = mulberry32(seed);
|
|
177
216
|
const viewport = pick(rng, VIEWPORTS);
|
|
178
217
|
const webgl = pick(rng, WEBGL_PROFILES);
|
|
179
|
-
const languages = [...pick(rng,
|
|
218
|
+
const languages = [...pick(rng, getLanguageProfiles())];
|
|
180
219
|
const hardwareConcurrency = pick(rng, HARDWARE_CONCURRENCIES);
|
|
181
220
|
const deviceMemory = pick(rng, DEVICE_MEMORIES);
|
|
182
221
|
const platformInfo = pick(rng, PLATFORM_VERSIONS);
|
|
183
222
|
const notABrand = pick(rng, NOT_A_BRAND_VARIANTS);
|
|
184
223
|
|
|
224
|
+
const major = dynamicChromeMajor;
|
|
185
225
|
const build = randInt(rng, 7300, 7600);
|
|
186
226
|
const patch = randInt(rng, 0, 160);
|
|
187
|
-
const chromeVersion = `${
|
|
227
|
+
const chromeVersion = `${major}.0.${build}.${patch}`;
|
|
188
228
|
|
|
189
229
|
const brands = [
|
|
190
230
|
{ brand: notABrand.brand, version: notABrand.version },
|
|
191
|
-
{ brand: "Google Chrome", version: String(
|
|
192
|
-
{ brand: "Chromium", version: String(
|
|
231
|
+
{ brand: "Google Chrome", version: String(major) },
|
|
232
|
+
{ brand: "Chromium", version: String(major) },
|
|
193
233
|
];
|
|
194
234
|
const fullVersionList = [
|
|
195
235
|
{ brand: notABrand.brand, version: chromeVersion },
|
|
@@ -7,7 +7,38 @@ import { chromium, type Browser, type BrowserContext, type Page } from "patchrig
|
|
|
7
7
|
import path from "path";
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import crypto from "crypto";
|
|
10
|
-
import
|
|
10
|
+
import { spawnSync } from "child_process";
|
|
11
|
+
import { createRequire } from "module";
|
|
12
|
+
|
|
13
|
+
const requireLocal = createRequire(import.meta.url);
|
|
14
|
+
|
|
15
|
+
function autoInstallPlaywrightChromium(): void {
|
|
16
|
+
const installEnv = {
|
|
17
|
+
...process.env,
|
|
18
|
+
PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT:
|
|
19
|
+
process.env.PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT || "300000",
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
const patchrightEntry = requireLocal.resolve("patchright");
|
|
23
|
+
const cliPath = path.join(path.dirname(patchrightEntry), "cli.js");
|
|
24
|
+
if (fs.existsSync(cliPath)) {
|
|
25
|
+
console.log("⏳ [Playwright] Navegador Chromium não encontrado. Instalando automaticamente...");
|
|
26
|
+
spawnSync(process.execPath, [cliPath, "install", "chromium"], {
|
|
27
|
+
stdio: "inherit",
|
|
28
|
+
env: installEnv,
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
} catch {}
|
|
33
|
+
|
|
34
|
+
console.log("⏳ [Playwright] Navegador Chromium não encontrado. Instalando via npx...");
|
|
35
|
+
const cmd = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
36
|
+
spawnSync(cmd, ["--yes", "patchright", "install", "chromium"], {
|
|
37
|
+
stdio: "inherit",
|
|
38
|
+
env: installEnv,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
import { loadAccounts, type QwenAccount } from "../core/accounts.ts";
|
|
11
42
|
// Imported here rather than injected from session-keeper.ts: account-concurrency
|
|
12
43
|
// only depends on config/logger, so playwright -> account-concurrency stays
|
|
13
44
|
// acyclic, while the reverse direction would drag the browser layer into core.
|
|
@@ -23,6 +54,7 @@ import { getAccountsByPriority } from "../core/account-priority.ts";
|
|
|
23
54
|
import {
|
|
24
55
|
clearFingerprintCache,
|
|
25
56
|
getFingerprintProfile,
|
|
57
|
+
updateChromeMajor,
|
|
26
58
|
type FingerprintProfile,
|
|
27
59
|
} from "./fingerprint.ts";
|
|
28
60
|
import { subtlePageActivity } from "./human-behavior.ts";
|
|
@@ -260,16 +292,38 @@ export async function getOrLaunchSharedBrowser(
|
|
|
260
292
|
const launchArgs = buildChromiumLaunchArgs(defaultViewport);
|
|
261
293
|
|
|
262
294
|
console.log(
|
|
263
|
-
|
|
295
|
+
`🌐 [Playwright] Launching single shared ${browserType} browser...`,
|
|
264
296
|
);
|
|
265
297
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
298
|
+
let browser: Browser;
|
|
299
|
+
try {
|
|
300
|
+
browser = await engine.launch({
|
|
301
|
+
headless,
|
|
302
|
+
channel,
|
|
303
|
+
ignoreDefaultArgs: ["--enable-automation", "--enable-blink-features"],
|
|
304
|
+
args: launchArgs,
|
|
305
|
+
});
|
|
306
|
+
} catch (launchErr: any) {
|
|
307
|
+
if (launchErr?.message?.includes("Executable doesn't exist")) {
|
|
308
|
+
autoInstallPlaywrightChromium();
|
|
309
|
+
browser = await engine.launch({
|
|
310
|
+
headless,
|
|
311
|
+
channel,
|
|
312
|
+
ignoreDefaultArgs: ["--enable-automation", "--enable-blink-features"],
|
|
313
|
+
args: launchArgs,
|
|
314
|
+
});
|
|
315
|
+
} else {
|
|
316
|
+
throw launchErr;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
272
319
|
|
|
320
|
+
try {
|
|
321
|
+
const v = browser.version();
|
|
322
|
+
const major = parseInt(v.split(".")[0], 10);
|
|
323
|
+
if (major >= 100) {
|
|
324
|
+
updateChromeMajor(major);
|
|
325
|
+
}
|
|
326
|
+
} catch {}
|
|
273
327
|
browser.on("disconnected", () => {
|
|
274
328
|
console.warn("[Playwright] Shared browser disconnected");
|
|
275
329
|
sharedBrowser = null;
|
|
@@ -2338,6 +2392,64 @@ export function pruneAllPlaywrightProfiles(baseDir = getProfilesDir()): {
|
|
|
2338
2392
|
|
|
2339
2393
|
return { totalFreedBytes, totalFreedFiles, profilesCleaned };
|
|
2340
2394
|
}
|
|
2395
|
+
/**
|
|
2396
|
+
* Removes profile directories in data/qwen_profiles that do not belong to any
|
|
2397
|
+
* active account configured in the database or environment, plus any lingering
|
|
2398
|
+
* .stale-* directories from previous lock renames.
|
|
2399
|
+
*/
|
|
2400
|
+
export function cleanupOrphanProfiles(
|
|
2401
|
+
baseDir = getProfilesDir(),
|
|
2402
|
+
activeAccountIds?: Set<string>,
|
|
2403
|
+
): {
|
|
2404
|
+
removedCount: number;
|
|
2405
|
+
freedBytes: number;
|
|
2406
|
+
} {
|
|
2407
|
+
let removedCount = 0;
|
|
2408
|
+
let freedBytes = 0;
|
|
2409
|
+
|
|
2410
|
+
try {
|
|
2411
|
+
if (!fs.existsSync(baseDir)) {
|
|
2412
|
+
return { removedCount, freedBytes };
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
const activeIds =
|
|
2416
|
+
activeAccountIds ?? new Set(loadAccounts().map((a) => a.id));
|
|
2417
|
+
|
|
2418
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
2419
|
+
for (const entry of entries) {
|
|
2420
|
+
if (!entry.isDirectory()) continue;
|
|
2421
|
+
const isStale = entry.name.includes(".stale-");
|
|
2422
|
+
const isOrphan = !isStale && !activeIds.has(entry.name);
|
|
2423
|
+
|
|
2424
|
+
if (isStale || isOrphan) {
|
|
2425
|
+
const targetPath = path.join(baseDir, entry.name);
|
|
2426
|
+
try {
|
|
2427
|
+
let bytes = 0;
|
|
2428
|
+
const countSize = (d: string) => {
|
|
2429
|
+
try {
|
|
2430
|
+
const subEntries = fs.readdirSync(d, { withFileTypes: true });
|
|
2431
|
+
for (const e of subEntries) {
|
|
2432
|
+
const full = path.join(d, e.name);
|
|
2433
|
+
if (e.isDirectory()) countSize(full);
|
|
2434
|
+
else if (e.isFile()) {
|
|
2435
|
+
try { bytes += fs.statSync(full).size; } catch {}
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
} catch {}
|
|
2439
|
+
};
|
|
2440
|
+
countSize(targetPath);
|
|
2441
|
+
removePlaywrightProfile(targetPath);
|
|
2442
|
+
if (!fs.existsSync(targetPath)) {
|
|
2443
|
+
removedCount++;
|
|
2444
|
+
freedBytes += bytes;
|
|
2445
|
+
}
|
|
2446
|
+
} catch {}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
} catch {}
|
|
2450
|
+
|
|
2451
|
+
return { removedCount, freedBytes };
|
|
2452
|
+
}
|
|
2341
2453
|
|
|
2342
2454
|
const PROFILE_RESET_TIMEOUT_MS = Math.max(90_000, config.timeouts.headers);
|
|
2343
2455
|
|
|
@@ -153,10 +153,10 @@ const WARM_POOL_LOW_WATER = 3;
|
|
|
153
153
|
|
|
154
154
|
function warmChatKey(
|
|
155
155
|
accountId: string | undefined,
|
|
156
|
-
|
|
156
|
+
_model: string,
|
|
157
157
|
chatId: string,
|
|
158
158
|
) {
|
|
159
|
-
return `${accountId || "global"}:${
|
|
159
|
+
return `${accountId || "global"}:${chatId}`;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
function markWarmChatInFlight(
|
|
@@ -183,8 +183,8 @@ function isWarmChatInFlight(
|
|
|
183
183
|
return inFlightWarmChats.has(warmChatKey(accountId, model, chatId));
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
function chatPoolKey(accountId: string | undefined,
|
|
187
|
-
return
|
|
186
|
+
function chatPoolKey(accountId: string | undefined, _model?: string): string {
|
|
187
|
+
return accountId || "global";
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
function isQwenChatPoolEnabled(): boolean {
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from "uuid";
|
|
2
2
|
import { qwenUrl, qwenOrigin } from "./qwen-url.ts";
|
|
3
3
|
import { config } from "../core/config.js";
|
|
4
|
+
import { getChromeMajor, getSystemLocale } from "./fingerprint.ts";
|
|
5
|
+
|
|
6
|
+
export function getSystemAcceptLanguage(customLocale?: string): string {
|
|
7
|
+
const locale = customLocale || getSystemLocale();
|
|
8
|
+
const primary = locale.split("-")[0];
|
|
9
|
+
if (locale.toLowerCase() === "en-us" || locale.toLowerCase() === "en") {
|
|
10
|
+
return "en-US,en;q=0.9";
|
|
11
|
+
}
|
|
12
|
+
if (locale.toLowerCase().startsWith("pt")) {
|
|
13
|
+
return "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7";
|
|
14
|
+
}
|
|
15
|
+
return `${locale},${primary};q=0.9,en-US;q=0.8,en;q=0.7`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getDefaultQwenUserAgent(major = getChromeMajor()): string {
|
|
19
|
+
return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${major}.0.0.0 Safari/537.36`;
|
|
20
|
+
}
|
|
21
|
+
|
|
4
22
|
let dynamicWebVersion: string | null = null;
|
|
5
23
|
|
|
6
24
|
export function updateQwenWebVersion(version?: string | null): void {
|
|
@@ -14,13 +32,12 @@ export function getQwenWebVersion(): string {
|
|
|
14
32
|
}
|
|
15
33
|
|
|
16
34
|
export const QWEN_WEB_VERSION = config.qwen.webVersion;
|
|
17
|
-
export const DEFAULT_QWEN_USER_AGENT =
|
|
18
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36";
|
|
35
|
+
export const DEFAULT_QWEN_USER_AGENT = getDefaultQwenUserAgent(151);
|
|
19
36
|
const QWEN_TIMEZONE_HEADER = new Date().toString().split(" (")[0];
|
|
20
|
-
|
|
21
37
|
export interface BuildQwenHeadersOptions {
|
|
22
38
|
cookie: string;
|
|
23
39
|
userAgent?: string;
|
|
40
|
+
acceptLanguage?: string;
|
|
24
41
|
bxUa?: string;
|
|
25
42
|
bxUmidtoken?: string;
|
|
26
43
|
bxV?: string;
|
|
@@ -40,7 +57,10 @@ export function buildQwenRequestHeaders(
|
|
|
40
57
|
const headers: Record<string, string> = {
|
|
41
58
|
...(opts.extra ?? {}),
|
|
42
59
|
Accept: "application/json",
|
|
43
|
-
"Accept-Language":
|
|
60
|
+
"Accept-Language":
|
|
61
|
+
opts.extra?.["Accept-Language"] ||
|
|
62
|
+
opts.acceptLanguage ||
|
|
63
|
+
getSystemAcceptLanguage(),
|
|
44
64
|
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
45
65
|
"Content-Type": "application/json",
|
|
46
66
|
Cookie: opts.cookie,
|
|
@@ -54,7 +74,7 @@ export function buildQwenRequestHeaders(
|
|
|
54
74
|
"Sec-Fetch-Mode": "cors",
|
|
55
75
|
"Sec-Fetch-Site": "same-origin",
|
|
56
76
|
Connection: "keep-alive",
|
|
57
|
-
"User-Agent": opts.userAgent ||
|
|
77
|
+
"User-Agent": opts.userAgent || getDefaultQwenUserAgent(),
|
|
58
78
|
"X-Request-Id": uuidv4(),
|
|
59
79
|
"bx-v": opts.bxV || "2.5.37",
|
|
60
80
|
source: "web",
|
|
@@ -62,7 +82,9 @@ export function buildQwenRequestHeaders(
|
|
|
62
82
|
timezone: opts.extra?.timezone || new Date().toString().split(" (")[0],
|
|
63
83
|
// Use the real browser client-hints when captured (anti-hardcoded); fall
|
|
64
84
|
// back to the static fingerprint otherwise.
|
|
65
|
-
"sec-ch-ua":
|
|
85
|
+
"sec-ch-ua":
|
|
86
|
+
opts.secChUa ||
|
|
87
|
+
`"Google Chrome";v="${getChromeMajor()}", "Chromium";v="${getChromeMajor()}", "Not.A/Brand";v="99"`,
|
|
66
88
|
"sec-ch-ua-mobile": opts.secChUaMobile || "?0",
|
|
67
89
|
"sec-ch-ua-platform": opts.secChUaPlatform || '"Windows"',
|
|
68
90
|
};
|