qwenproxy-cli 1.0.5 → 1.0.7
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/bin/qwenproxy.js +7 -0
- package/package.json +1 -1
- package/src/core/config.ts +2 -0
- package/src/core/model-alias.ts +47 -7
- package/src/index.ts +6 -0
- package/src/routes/chat/validation.ts +1 -1
- package/src/services/fingerprint.ts +45 -5
- package/src/services/human-behavior.ts +36 -20
- package/src/services/playwright.ts +122 -16
- 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/views/chat-view.ts +1 -0
- package/src/update-cli.ts +21 -6
package/bin/qwenproxy.js
CHANGED
|
@@ -134,15 +134,22 @@ if (isBrowserCommand) {
|
|
|
134
134
|
const patchrightEntry = require.resolve("patchright");
|
|
135
135
|
cliPath = path.join(path.dirname(patchrightEntry), "cli.js");
|
|
136
136
|
} catch {}
|
|
137
|
+
const installEnv = {
|
|
138
|
+
...process.env,
|
|
139
|
+
PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT:
|
|
140
|
+
process.env.PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT || "300000",
|
|
141
|
+
};
|
|
137
142
|
|
|
138
143
|
if (cliPath && fs.existsSync(cliPath)) {
|
|
139
144
|
spawnSync(process.execPath, [cliPath, "install", "chromium"], {
|
|
140
145
|
stdio: "inherit",
|
|
146
|
+
env: installEnv,
|
|
141
147
|
});
|
|
142
148
|
} else {
|
|
143
149
|
const cmd = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
144
150
|
spawnSync(cmd, ["--yes", "patchright", "install", "chromium"], {
|
|
145
151
|
stdio: "inherit",
|
|
152
|
+
env: installEnv,
|
|
146
153
|
});
|
|
147
154
|
}
|
|
148
155
|
console.log("✓ [QwenProxy] Navegador instalado com sucesso!\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
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/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/index.ts
CHANGED
|
@@ -21,6 +21,9 @@ process.on('uncaughtException', (error: unknown) => {
|
|
|
21
21
|
msg.includes('Target page, context or browser has been closed') ||
|
|
22
22
|
msg.includes('Browser has been closed') ||
|
|
23
23
|
msg.includes('Target closed') ||
|
|
24
|
+
msg.includes('Target crashed') ||
|
|
25
|
+
msg.includes('Page crashed') ||
|
|
26
|
+
msg.includes('Assertion error') ||
|
|
24
27
|
msg.includes('Connection closed')
|
|
25
28
|
) {
|
|
26
29
|
console.warn(`⚠️ [Playwright] Handled benign driver teardown exception: ${msg}`)
|
|
@@ -36,6 +39,9 @@ process.on('unhandledRejection', (reason: unknown) => {
|
|
|
36
39
|
msg.includes('Target page, context or browser has been closed') ||
|
|
37
40
|
msg.includes('Browser has been closed') ||
|
|
38
41
|
msg.includes('Target closed') ||
|
|
42
|
+
msg.includes('Target crashed') ||
|
|
43
|
+
msg.includes('Page crashed') ||
|
|
44
|
+
msg.includes('Assertion error') ||
|
|
39
45
|
msg.includes('Connection closed')
|
|
40
46
|
) {
|
|
41
47
|
console.warn(`⚠️ [Playwright] Handled benign driver teardown rejection: ${msg}`)
|
|
@@ -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 },
|
|
@@ -147,27 +147,43 @@ export async function subtlePageActivity(page: Page): Promise<void> {
|
|
|
147
147
|
const viewport = page.viewportSize();
|
|
148
148
|
if (!viewport) return;
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
try {
|
|
151
|
+
const x = Math.floor(viewport.width * (0.25 + Math.random() * 0.5));
|
|
152
|
+
const y = Math.floor(viewport.height * (0.25 + Math.random() * 0.5));
|
|
153
|
+
await page.mouse.move(x, y, { steps: 6 + Math.floor(Math.random() * 8) });
|
|
153
154
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
if (Math.random() < 0.35) {
|
|
156
|
+
await page.mouse.wheel(0, Math.random() < 0.5 ? 60 : -60).catch(() => {});
|
|
157
|
+
}
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
159
|
+
await page
|
|
160
|
+
.evaluate(() => {
|
|
161
|
+
try {
|
|
162
|
+
const target = document.querySelector(
|
|
163
|
+
'[data-testid="sidebar"], .sidebar, nav, aside, main',
|
|
164
|
+
);
|
|
165
|
+
if (target) {
|
|
166
|
+
target.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
|
167
|
+
target.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Best-effort keep-alive only.
|
|
167
171
|
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
.
|
|
172
|
+
})
|
|
173
|
+
.catch(() => {});
|
|
174
|
+
} catch (err: any) {
|
|
175
|
+
if (page.isClosed()) return;
|
|
176
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
+
if (
|
|
178
|
+
msg.includes("Target page, context or browser has been closed") ||
|
|
179
|
+
msg.includes("Browser has been closed") ||
|
|
180
|
+
msg.includes("Target closed") ||
|
|
181
|
+
msg.includes("Target crashed") ||
|
|
182
|
+
msg.includes("Page crashed") ||
|
|
183
|
+
msg.includes("Connection closed")
|
|
184
|
+
) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
173
189
|
}
|
|
@@ -13,6 +13,11 @@ import { createRequire } from "module";
|
|
|
13
13
|
const requireLocal = createRequire(import.meta.url);
|
|
14
14
|
|
|
15
15
|
function autoInstallPlaywrightChromium(): void {
|
|
16
|
+
const installEnv = {
|
|
17
|
+
...process.env,
|
|
18
|
+
PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT:
|
|
19
|
+
process.env.PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT || "300000",
|
|
20
|
+
};
|
|
16
21
|
try {
|
|
17
22
|
const patchrightEntry = requireLocal.resolve("patchright");
|
|
18
23
|
const cliPath = path.join(path.dirname(patchrightEntry), "cli.js");
|
|
@@ -20,6 +25,7 @@ function autoInstallPlaywrightChromium(): void {
|
|
|
20
25
|
console.log("⏳ [Playwright] Navegador Chromium não encontrado. Instalando automaticamente...");
|
|
21
26
|
spawnSync(process.execPath, [cliPath, "install", "chromium"], {
|
|
22
27
|
stdio: "inherit",
|
|
28
|
+
env: installEnv,
|
|
23
29
|
});
|
|
24
30
|
return;
|
|
25
31
|
}
|
|
@@ -29,6 +35,7 @@ function autoInstallPlaywrightChromium(): void {
|
|
|
29
35
|
const cmd = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
30
36
|
spawnSync(cmd, ["--yes", "patchright", "install", "chromium"], {
|
|
31
37
|
stdio: "inherit",
|
|
38
|
+
env: installEnv,
|
|
32
39
|
});
|
|
33
40
|
}
|
|
34
41
|
import { loadAccounts, type QwenAccount } from "../core/accounts.ts";
|
|
@@ -47,6 +54,7 @@ import { getAccountsByPriority } from "../core/account-priority.ts";
|
|
|
47
54
|
import {
|
|
48
55
|
clearFingerprintCache,
|
|
49
56
|
getFingerprintProfile,
|
|
57
|
+
updateChromeMajor,
|
|
50
58
|
type FingerprintProfile,
|
|
51
59
|
} from "./fingerprint.ts";
|
|
52
60
|
import { subtlePageActivity } from "./human-behavior.ts";
|
|
@@ -309,6 +317,13 @@ export async function getOrLaunchSharedBrowser(
|
|
|
309
317
|
}
|
|
310
318
|
}
|
|
311
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 {}
|
|
312
327
|
browser.on("disconnected", () => {
|
|
313
328
|
console.warn("[Playwright] Shared browser disconnected");
|
|
314
329
|
sharedBrowser = null;
|
|
@@ -1186,12 +1201,13 @@ export async function initPlaywrightForAccount(
|
|
|
1186
1201
|
// If a context limit is configured, make room by closing idle contexts.
|
|
1187
1202
|
await evictIdlePlaywrightContextsToLimit().catch(() => {});
|
|
1188
1203
|
|
|
1204
|
+
const profilePath = getAccountProfilePath(account.id);
|
|
1189
1205
|
const fingerprint = getFingerprintProfile(account.id);
|
|
1190
|
-
const
|
|
1191
|
-
const storageState = loadStorageState(account.id);
|
|
1206
|
+
const { engine, channel } = resolveBrowserEngine(browserType);
|
|
1192
1207
|
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1208
|
+
const launchOptions = {
|
|
1209
|
+
headless,
|
|
1210
|
+
channel,
|
|
1195
1211
|
userAgent: fingerprint.userAgent,
|
|
1196
1212
|
locale: fingerprint.locale,
|
|
1197
1213
|
timezoneId: fingerprint.timezoneId,
|
|
@@ -1200,13 +1216,37 @@ export async function initPlaywrightForAccount(
|
|
|
1200
1216
|
deviceScaleFactor: 1,
|
|
1201
1217
|
isMobile: false,
|
|
1202
1218
|
hasTouch: false,
|
|
1203
|
-
colorScheme: "light",
|
|
1219
|
+
colorScheme: "light" as const,
|
|
1204
1220
|
extraHTTPHeaders: {
|
|
1205
1221
|
"sec-ch-ua": fingerprint.secChUa,
|
|
1206
1222
|
"sec-ch-ua-mobile": "?0",
|
|
1207
1223
|
"sec-ch-ua-platform": '"Windows"',
|
|
1208
1224
|
},
|
|
1209
|
-
|
|
1225
|
+
ignoreDefaultArgs: ["--enable-automation", "--enable-blink-features"],
|
|
1226
|
+
args: buildChromiumLaunchArgs(fingerprint.viewport),
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1229
|
+
let acctContext: BrowserContext;
|
|
1230
|
+
try {
|
|
1231
|
+
acctContext = await engine.launchPersistentContext(profilePath, launchOptions);
|
|
1232
|
+
} catch (launchErr: any) {
|
|
1233
|
+
if (launchErr?.message?.includes("Executable doesn't exist")) {
|
|
1234
|
+
autoInstallPlaywrightChromium();
|
|
1235
|
+
acctContext = await engine.launchPersistentContext(profilePath, launchOptions);
|
|
1236
|
+
} else {
|
|
1237
|
+
throw launchErr;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
try {
|
|
1242
|
+
const v = acctContext.browser()?.version();
|
|
1243
|
+
if (v) {
|
|
1244
|
+
const major = parseInt(v.split(".")[0], 10);
|
|
1245
|
+
if (major >= 100) {
|
|
1246
|
+
updateChromeMajor(major);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
} catch {}
|
|
1210
1250
|
|
|
1211
1251
|
try {
|
|
1212
1252
|
// Comprehensive stealth scripts for anti-bot evasion
|
|
@@ -1215,7 +1255,38 @@ export async function initPlaywrightForAccount(
|
|
|
1215
1255
|
await hook(acctContext);
|
|
1216
1256
|
}
|
|
1217
1257
|
|
|
1218
|
-
|
|
1258
|
+
// If native profile cookies are empty but a backup storage_state.json exists, restore cookies
|
|
1259
|
+
const storageState = loadStorageState(account.id);
|
|
1260
|
+
if (storageState) {
|
|
1261
|
+
try {
|
|
1262
|
+
const raw = fs.readFileSync(storageState, "utf8");
|
|
1263
|
+
const parsed = JSON.parse(raw);
|
|
1264
|
+
if (Array.isArray(parsed.cookies) && parsed.cookies.length > 0) {
|
|
1265
|
+
const currentCookies = await acctContext.cookies();
|
|
1266
|
+
if (currentCookies.length === 0) {
|
|
1267
|
+
await acctContext.addCookies(parsed.cookies).catch(() => {});
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
} catch {}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Persistent contexts may already contain an initial about:blank tab.
|
|
1274
|
+
// Reuse it instead of creating a second tab. Prefer a tab already on the
|
|
1275
|
+
// Qwen origin when one exists.
|
|
1276
|
+
const existingPages = acctContext.pages().filter((p) => !p.isClosed());
|
|
1277
|
+
const acctPage =
|
|
1278
|
+
existingPages.find((p) => p.url().startsWith(qwenOrigin())) ??
|
|
1279
|
+
existingPages[0] ??
|
|
1280
|
+
(await acctContext.newPage());
|
|
1281
|
+
|
|
1282
|
+
// Close any extra blank tabs that may have been created by the browser
|
|
1283
|
+
// profile/startup, but keep the primary page selected above.
|
|
1284
|
+
for (const extraPage of existingPages.slice(1)) {
|
|
1285
|
+
if (extraPage !== acctPage && extraPage.url() === "about:blank") {
|
|
1286
|
+
await extraPage.close({ runBeforeUnload: false }).catch(() => {});
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1219
1290
|
acctPage.setDefaultTimeout(config.timeouts.page);
|
|
1220
1291
|
acctPage.setDefaultNavigationTimeout(config.timeouts.navigation);
|
|
1221
1292
|
accountContexts.set(account.id, acctContext);
|
|
@@ -1330,13 +1401,12 @@ export async function validateAccountLogin(
|
|
|
1330
1401
|
);
|
|
1331
1402
|
try {
|
|
1332
1403
|
if (accountPages.has(account.id)) return true;
|
|
1333
|
-
|
|
1404
|
+
const profilePath = getAccountProfilePath(account.id);
|
|
1334
1405
|
const fingerprint = getFingerprintProfile(account.id);
|
|
1335
|
-
const
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
...(storageState ? { storageState } : {}),
|
|
1406
|
+
const { engine, channel } = resolveBrowserEngine(browserType);
|
|
1407
|
+
const launchOptions = {
|
|
1408
|
+
headless,
|
|
1409
|
+
channel,
|
|
1340
1410
|
userAgent: fingerprint.userAgent,
|
|
1341
1411
|
locale: fingerprint.locale,
|
|
1342
1412
|
timezoneId: fingerprint.timezoneId,
|
|
@@ -1345,18 +1415,51 @@ export async function validateAccountLogin(
|
|
|
1345
1415
|
deviceScaleFactor: 1,
|
|
1346
1416
|
isMobile: false,
|
|
1347
1417
|
hasTouch: false,
|
|
1348
|
-
colorScheme: "light",
|
|
1418
|
+
colorScheme: "light" as const,
|
|
1349
1419
|
extraHTTPHeaders: {
|
|
1350
1420
|
"sec-ch-ua": fingerprint.secChUa,
|
|
1351
1421
|
"sec-ch-ua-mobile": "?0",
|
|
1352
1422
|
"sec-ch-ua-platform": '"Windows"',
|
|
1353
1423
|
},
|
|
1354
|
-
|
|
1424
|
+
ignoreDefaultArgs: ["--enable-automation", "--enable-blink-features"],
|
|
1425
|
+
args: buildChromiumLaunchArgs(fingerprint.viewport),
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
let acctContext: BrowserContext;
|
|
1429
|
+
try {
|
|
1430
|
+
acctContext = await engine.launchPersistentContext(profilePath, launchOptions);
|
|
1431
|
+
} catch (launchErr: any) {
|
|
1432
|
+
if (launchErr?.message?.includes("Executable doesn't exist")) {
|
|
1433
|
+
autoInstallPlaywrightChromium();
|
|
1434
|
+
acctContext = await engine.launchPersistentContext(profilePath, launchOptions);
|
|
1435
|
+
} else {
|
|
1436
|
+
throw launchErr;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1355
1439
|
|
|
1356
1440
|
try {
|
|
1357
1441
|
await acctContext.addInitScript(getStealthScript(fingerprint));
|
|
1358
|
-
const acctPage = await acctContext.newPage();
|
|
1359
1442
|
|
|
1443
|
+
// If native profile cookies are empty but a backup storage_state.json exists, restore cookies
|
|
1444
|
+
const storageState = loadStorageState(account.id);
|
|
1445
|
+
if (storageState) {
|
|
1446
|
+
try {
|
|
1447
|
+
const raw = fs.readFileSync(storageState, "utf8");
|
|
1448
|
+
const parsed = JSON.parse(raw);
|
|
1449
|
+
if (Array.isArray(parsed.cookies) && parsed.cookies.length > 0) {
|
|
1450
|
+
const currentCookies = await acctContext.cookies();
|
|
1451
|
+
if (currentCookies.length === 0) {
|
|
1452
|
+
await acctContext.addCookies(parsed.cookies).catch(() => {});
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
} catch {}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const existingPages = acctContext.pages().filter((p) => !p.isClosed());
|
|
1459
|
+
const acctPage =
|
|
1460
|
+
existingPages.find((p) => p.url().startsWith(qwenOrigin())) ??
|
|
1461
|
+
existingPages[0] ??
|
|
1462
|
+
(await acctContext.newPage());
|
|
1360
1463
|
// Check if already logged in via cookies
|
|
1361
1464
|
const cookies = await acctContext.cookies();
|
|
1362
1465
|
const hasAuthCookie = cookies.some(
|
|
@@ -2774,6 +2877,9 @@ export function isPlaywrightAlreadyClosedError(error: unknown): boolean {
|
|
|
2774
2877
|
message.includes("Target page, context or browser has been closed") ||
|
|
2775
2878
|
message.includes("Browser has been closed") ||
|
|
2776
2879
|
message.includes("Target closed") ||
|
|
2880
|
+
message.includes("Target crashed") ||
|
|
2881
|
+
message.includes("Page crashed") ||
|
|
2882
|
+
message.includes("Assertion error") ||
|
|
2777
2883
|
message.includes("Cannot find parent object") ||
|
|
2778
2884
|
message.includes("Connection closed")
|
|
2779
2885
|
);
|
|
@@ -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
|
};
|
package/src/sync/omp.ts
CHANGED
|
@@ -3,7 +3,30 @@ import path from "node:path";
|
|
|
3
3
|
import type { ClientSyncResult, SyncOptions } from "./types.ts";
|
|
4
4
|
import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
|
|
5
5
|
|
|
6
|
-
function buildOmpProviderYaml(
|
|
6
|
+
function buildOmpProviderYaml(
|
|
7
|
+
baseUrl: string,
|
|
8
|
+
apiKey: string,
|
|
9
|
+
primaryModel: string = "qwen3.8-max",
|
|
10
|
+
): string {
|
|
11
|
+
const modelList = [primaryModel];
|
|
12
|
+
if (primaryModel !== "qwen3.7-plus") {
|
|
13
|
+
modelList.push("qwen3.7-plus");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const formattedModels = modelList
|
|
17
|
+
.map(
|
|
18
|
+
(m) => ` - id: ${m}
|
|
19
|
+
name: ${m === "qwen3.8-max" ? "Qwen3.8-Max" : m === "qwen3.7-plus" ? "Qwen3.7-Plus" : m}
|
|
20
|
+
input: [text, image]
|
|
21
|
+
contextWindow: 1000000
|
|
22
|
+
maxTokens: 131072
|
|
23
|
+
reasoning: true
|
|
24
|
+
thinking:
|
|
25
|
+
mode: effort
|
|
26
|
+
efforts: [low, medium, high]`,
|
|
27
|
+
)
|
|
28
|
+
.join("\n");
|
|
29
|
+
|
|
7
30
|
return ` qwenproxy:
|
|
8
31
|
baseUrl: ${baseUrl}
|
|
9
32
|
api: openai-completions
|
|
@@ -13,29 +36,12 @@ function buildOmpProviderYaml(baseUrl: string, apiKey: string): string {
|
|
|
13
36
|
supportsReasoningEffort: true
|
|
14
37
|
maxTokensField: max_completion_tokens
|
|
15
38
|
models:
|
|
16
|
-
|
|
17
|
-
name: Qwen3.8-Max
|
|
18
|
-
input: [text, image]
|
|
19
|
-
contextWindow: 1000000
|
|
20
|
-
maxTokens: 131072
|
|
21
|
-
reasoning: true
|
|
22
|
-
thinking:
|
|
23
|
-
mode: effort
|
|
24
|
-
efforts: [low, medium, high]
|
|
25
|
-
- id: qwen3.7-plus
|
|
26
|
-
name: Qwen3.7-Plus
|
|
27
|
-
input: [text, image]
|
|
28
|
-
contextWindow: 1000000
|
|
29
|
-
maxTokens: 131072
|
|
30
|
-
reasoning: true
|
|
31
|
-
thinking:
|
|
32
|
-
mode: effort
|
|
33
|
-
efforts: [low, medium, high]
|
|
39
|
+
${formattedModels}
|
|
34
40
|
`;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export function syncOmp(options: SyncOptions): ClientSyncResult {
|
|
38
|
-
const { filePath, apiKey, baseUrl } = options;
|
|
44
|
+
const { filePath, apiKey, baseUrl, model = "qwen3.8-max" } = options;
|
|
39
45
|
try {
|
|
40
46
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
41
47
|
|
|
@@ -47,7 +53,7 @@ export function syncOmp(options: SyncOptions): ClientSyncResult {
|
|
|
47
53
|
content = fs.readFileSync(filePath, "utf-8");
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
const providerBlock = buildOmpProviderYaml(baseUrl, apiKey);
|
|
56
|
+
const providerBlock = buildOmpProviderYaml(baseUrl, apiKey, model);
|
|
51
57
|
|
|
52
58
|
if (!content.trim()) {
|
|
53
59
|
content = `providers:\n${providerBlock}`;
|
package/src/sync/opencode.ts
CHANGED
|
@@ -3,7 +3,36 @@ import path from "node:path";
|
|
|
3
3
|
import type { ClientSyncResult, SyncOptions } from "./types.ts";
|
|
4
4
|
import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
|
|
5
5
|
|
|
6
|
-
function buildOpenCodeProviderObject(
|
|
6
|
+
function buildOpenCodeProviderObject(
|
|
7
|
+
baseUrl: string,
|
|
8
|
+
apiKey: string,
|
|
9
|
+
primaryModel: string = "qwen3.8-max",
|
|
10
|
+
): Record<string, any> {
|
|
11
|
+
const modelsObj: Record<string, any> = {};
|
|
12
|
+
const modelList = [primaryModel];
|
|
13
|
+
if (primaryModel !== "qwen3.7-plus") {
|
|
14
|
+
modelList.push("qwen3.7-plus");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const m of modelList) {
|
|
18
|
+
modelsObj[m] = {
|
|
19
|
+
name:
|
|
20
|
+
m === "qwen3.8-max"
|
|
21
|
+
? "Qwen 3.8 Max"
|
|
22
|
+
: m === "qwen3.7-plus"
|
|
23
|
+
? "Qwen 3.7 Plus"
|
|
24
|
+
: m,
|
|
25
|
+
limit: { context: 1048576, output: 65536 },
|
|
26
|
+
modalities: { input: ["text", "image"], output: ["text"] },
|
|
27
|
+
reasoning: true,
|
|
28
|
+
variants: {
|
|
29
|
+
low: { effort: "low" },
|
|
30
|
+
medium: { effort: "medium" },
|
|
31
|
+
high: { effort: "high" },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
7
36
|
return {
|
|
8
37
|
npm: "@ai-sdk/openai-compatible",
|
|
9
38
|
name: "QwenProxy",
|
|
@@ -11,30 +40,7 @@ function buildOpenCodeProviderObject(baseUrl: string, apiKey: string): Record<st
|
|
|
11
40
|
baseURL: baseUrl,
|
|
12
41
|
apiKey: apiKey,
|
|
13
42
|
},
|
|
14
|
-
models:
|
|
15
|
-
"qwen3.8-max": {
|
|
16
|
-
name: "Qwen 3.8 Max",
|
|
17
|
-
limit: { context: 1048576, output: 65536 },
|
|
18
|
-
modalities: { input: ["text", "image"], output: ["text"] },
|
|
19
|
-
reasoning: true,
|
|
20
|
-
variants: {
|
|
21
|
-
low: { effort: "low" },
|
|
22
|
-
medium: { effort: "medium" },
|
|
23
|
-
high: { effort: "high" },
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
"qwen3.7-plus": {
|
|
27
|
-
name: "Qwen 3.7 Plus",
|
|
28
|
-
limit: { context: 1048576, output: 65536 },
|
|
29
|
-
modalities: { input: ["text", "image"], output: ["text"] },
|
|
30
|
-
reasoning: true,
|
|
31
|
-
variants: {
|
|
32
|
-
low: { effort: "low" },
|
|
33
|
-
medium: { effort: "medium" },
|
|
34
|
-
high: { effort: "high" },
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
},
|
|
43
|
+
models: modelsObj,
|
|
38
44
|
};
|
|
39
45
|
}
|
|
40
46
|
function findKeyObjectSpan(content: string, key: string): { start: number; end: number; hasTrailingComma: boolean } | null {
|
|
@@ -121,10 +127,8 @@ function findKeyObjectSpan(content: string, key: string): { start: number; end:
|
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
export function syncOpenCode(options: SyncOptions): ClientSyncResult {
|
|
124
|
-
const { filePath, apiKey, baseUrl } = options;
|
|
130
|
+
const { filePath, apiKey, baseUrl, model = "qwen3.8-max" } = options;
|
|
125
131
|
try {
|
|
126
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
127
|
-
|
|
128
132
|
let backupPath: string | undefined;
|
|
129
133
|
let content = "";
|
|
130
134
|
|
|
@@ -133,10 +137,10 @@ export function syncOpenCode(options: SyncOptions): ClientSyncResult {
|
|
|
133
137
|
content = fs.readFileSync(filePath, "utf-8");
|
|
134
138
|
}
|
|
135
139
|
|
|
136
|
-
const providerObj = buildOpenCodeProviderObject(baseUrl, apiKey);
|
|
140
|
+
const providerObj = buildOpenCodeProviderObject(baseUrl, apiKey, model);
|
|
137
141
|
const providerJson = JSON.stringify(providerObj, null, 6)
|
|
138
142
|
.split("\n")
|
|
139
|
-
.map((line, idx) => (idx === 0 ? line :
|
|
143
|
+
.map((line, idx) => (idx === 0 ? line : ` ${line}`))
|
|
140
144
|
.join("\n");
|
|
141
145
|
|
|
142
146
|
const qwenEntry = ` "qwenproxy": ${providerJson}`;
|
package/src/tui/app.ts
CHANGED
|
@@ -11,7 +11,7 @@ import fs from "node:fs";
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
|
|
14
|
-
let cachedAppVersion = "
|
|
14
|
+
let cachedAppVersion = "";
|
|
15
15
|
try {
|
|
16
16
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
const pkgPath = path.resolve(currentDir, "../../package.json");
|
package/src/tui/proxy-client.ts
CHANGED
|
@@ -279,66 +279,67 @@ export async function streamChatCompletions(
|
|
|
279
279
|
* Fetches all live models dynamically from the running proxy /v1/models catalog.
|
|
280
280
|
*/
|
|
281
281
|
let cachedLiveModels: string[] | null = null;
|
|
282
|
-
let
|
|
282
|
+
let liveModelsPromise: Promise<string[]> | null = null;
|
|
283
283
|
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
const DEFAULT_FALLBACK_MODELS = [
|
|
285
|
+
"qwen3.8-max",
|
|
286
|
+
"qwen3.7-plus",
|
|
287
|
+
"qwen3.7-max",
|
|
288
|
+
"z-image-turbo",
|
|
289
|
+
"qwen-image-3.0-pro",
|
|
290
|
+
"qwen-image-3.0",
|
|
291
|
+
"wan2.7-image-pro",
|
|
292
|
+
"wan2.7-image",
|
|
293
|
+
"wan3.0-video",
|
|
294
|
+
"wan2.7-t2v",
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
export async function fetchLiveModels(forceRefresh = false): Promise<string[]> {
|
|
298
|
+
if (!forceRefresh && cachedLiveModels && cachedLiveModels.length > 0) {
|
|
286
299
|
return cachedLiveModels;
|
|
287
300
|
}
|
|
288
301
|
|
|
302
|
+
if (liveModelsPromise) {
|
|
303
|
+
return liveModelsPromise;
|
|
304
|
+
}
|
|
305
|
+
|
|
289
306
|
const port = config.server?.port || 7936;
|
|
290
307
|
const configuredHost = config.server?.host;
|
|
291
308
|
const host = configuredHost && configuredHost !== "0.0.0.0" ? configuredHost : "127.0.0.1";
|
|
292
309
|
const apiKey = config.apiKey || "sk-qwenproxy-local";
|
|
293
310
|
|
|
294
|
-
|
|
295
|
-
isFetchingLiveModels = true;
|
|
311
|
+
liveModelsPromise = (async () => {
|
|
296
312
|
const controller = new AbortController();
|
|
297
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
313
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
314
|
+
try {
|
|
315
|
+
const resp = await fetch(`http://${host}:${port}/v1/models`, {
|
|
316
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
317
|
+
signal: controller.signal,
|
|
318
|
+
});
|
|
319
|
+
if (resp.ok) {
|
|
320
|
+
const json = (await resp.json()) as any;
|
|
321
|
+
if (Array.isArray(json?.data)) {
|
|
322
|
+
const models = json.data
|
|
323
|
+
.map((m: any) => m.id)
|
|
324
|
+
.filter((id: any): id is string => typeof id === "string" && id.trim().length > 0)
|
|
325
|
+
.filter(
|
|
326
|
+
(id: string) =>
|
|
327
|
+
!id.endsWith("-fast") &&
|
|
328
|
+
!id.endsWith("-thinking") &&
|
|
329
|
+
!id.endsWith("-no-thinking"),
|
|
330
|
+
);
|
|
331
|
+
if (models.length > 0) {
|
|
332
|
+
cachedLiveModels = Array.from(new Set(models));
|
|
333
|
+
return cachedLiveModels;
|
|
319
334
|
}
|
|
320
335
|
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
336
|
+
}
|
|
337
|
+
} catch {} finally {
|
|
338
|
+
clearTimeout(timeout);
|
|
339
|
+
liveModelsPromise = null;
|
|
340
|
+
}
|
|
341
|
+
return cachedLiveModels || DEFAULT_FALLBACK_MODELS;
|
|
342
|
+
})();
|
|
329
343
|
|
|
330
|
-
return
|
|
331
|
-
cachedLiveModels || [
|
|
332
|
-
"qwen3.8-max",
|
|
333
|
-
"qwen3.7-plus",
|
|
334
|
-
"qwen3.7-max",
|
|
335
|
-
"z-image-turbo",
|
|
336
|
-
"qwen-image-3.0-pro",
|
|
337
|
-
"qwen-image-3.0",
|
|
338
|
-
"wan2.7-image-pro",
|
|
339
|
-
"wan2.7-image",
|
|
340
|
-
"wan3.0-video",
|
|
341
|
-
"wan2.7-t2v",
|
|
342
|
-
]
|
|
343
|
-
);
|
|
344
|
+
return liveModelsPromise;
|
|
344
345
|
}
|
|
@@ -341,6 +341,7 @@ export class ChatView implements TuiView {
|
|
|
341
341
|
(key.ctrl && key.name === "o") ||
|
|
342
342
|
(key.meta && key.name === "m")
|
|
343
343
|
) {
|
|
344
|
+
void this.refreshModels();
|
|
344
345
|
this.isModelModalOpen = true;
|
|
345
346
|
this.modalSelectedIndex = this.selectedModelIndex;
|
|
346
347
|
this.onNeedsRender?.();
|
package/src/update-cli.ts
CHANGED
|
@@ -27,17 +27,32 @@ export function detectPackageManager(): PackageManager {
|
|
|
27
27
|
|
|
28
28
|
return "npm";
|
|
29
29
|
}
|
|
30
|
-
export function getUpdateArgs(
|
|
30
|
+
export function getUpdateArgs(
|
|
31
|
+
pm: PackageManager,
|
|
32
|
+
packageName: string,
|
|
33
|
+
targetVersion?: string,
|
|
34
|
+
): { cmd: string; args: string[] } {
|
|
35
|
+
const versionTag = targetVersion ? `@${targetVersion}` : "@latest";
|
|
31
36
|
switch (pm) {
|
|
32
37
|
case "bun":
|
|
33
|
-
return { cmd: "bun", args: ["add", "-g", `${packageName}
|
|
38
|
+
return { cmd: "bun", args: ["add", "-g", `${packageName}${versionTag}`] };
|
|
34
39
|
case "pnpm":
|
|
35
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
cmd: "pnpm",
|
|
42
|
+
args: targetVersion
|
|
43
|
+
? ["add", "-g", `${packageName}@${targetVersion}`]
|
|
44
|
+
: ["update", "-g", packageName],
|
|
45
|
+
};
|
|
36
46
|
case "yarn":
|
|
37
|
-
return {
|
|
47
|
+
return {
|
|
48
|
+
cmd: "yarn",
|
|
49
|
+
args: targetVersion
|
|
50
|
+
? ["global", "add", `${packageName}@${targetVersion}`]
|
|
51
|
+
: ["global", "upgrade", packageName],
|
|
52
|
+
};
|
|
38
53
|
case "npm":
|
|
39
54
|
default:
|
|
40
|
-
return { cmd: "npm", args: ["install", "-g", `${packageName}
|
|
55
|
+
return { cmd: "npm", args: ["install", "-g", `${packageName}${versionTag}`] };
|
|
41
56
|
}
|
|
42
57
|
}
|
|
43
58
|
|
|
@@ -93,7 +108,7 @@ export async function runUpdateCommand(): Promise<void> {
|
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
console.log(`\n🚀 Nova versão disponível: v${currentVersion} ➔ v${latestVersion}`);
|
|
96
|
-
const { cmd, args } = getUpdateArgs(pm, packageName);
|
|
111
|
+
const { cmd, args } = getUpdateArgs(pm, packageName, latestVersion);
|
|
97
112
|
console.log(`⏳ Atualizando globalmente via ${pm} (${cmd} ${args.join(" ")})...`);
|
|
98
113
|
const fullUpdateCmd = `${cmd} ${args.join(" ")}`;
|
|
99
114
|
const updateProc = spawnSync(fullUpdateCmd, {
|