qwenproxy-cli 1.4.7 → 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.
- package/dist/clean-cache.js +38 -5
- package/dist/delete-chats.js +262 -74
- package/dist/index.js +327 -111
- package/dist/login.js +169 -59
- package/dist/reset-cooldowns.js +25 -5
- package/package.json +1 -1
- package/src/core/account-manager.ts +9 -0
- package/src/core/database.ts +10 -5
- package/src/core/server-log-buffer.ts +10 -3
- package/src/core/version.ts +1 -1
- package/src/routes/chat/account.ts +22 -10
- package/src/routes/chat/helpers.ts +29 -1
- package/src/routes/chat/retry-policy.ts +9 -8
- package/src/routes/chat/streaming.ts +12 -5
- package/src/routes/upload.ts +19 -14
- package/src/services/playwright.ts +180 -58
- package/src/services/qwen-chat-pool.ts +18 -5
- package/src/services/qwen-headers.ts +2 -1
- package/src/services/qwen.ts +65 -15
package/dist/login.js
CHANGED
|
@@ -359,6 +359,7 @@ function runMigrations(db2) {
|
|
|
359
359
|
"sec_ch_ua_mobile TEXT",
|
|
360
360
|
"sec_ch_ua_platform TEXT",
|
|
361
361
|
"version TEXT",
|
|
362
|
+
"refresh_token TEXT",
|
|
362
363
|
"captured_at INTEGER DEFAULT 0"
|
|
363
364
|
];
|
|
364
365
|
for (const col of authSessionCols) {
|
|
@@ -437,11 +438,11 @@ function saveAuthSession(accountId, session) {
|
|
|
437
438
|
INSERT OR REPLACE INTO qwen_auth_sessions (
|
|
438
439
|
account_id, cookie, user_agent, bx_v, bx_ua, bx_umidtoken,
|
|
439
440
|
sec_ch_ua, sec_ch_ua_mobile, sec_ch_ua_platform, version,
|
|
440
|
-
user_id, token_expires_at, captured_at, updated_at
|
|
441
|
+
user_id, token_expires_at, refresh_token, captured_at, updated_at
|
|
441
442
|
) VALUES (
|
|
442
443
|
?, ?, ?, ?, ?, ?,
|
|
443
444
|
?, ?, ?, ?,
|
|
444
|
-
?, ?, ?, datetime('now')
|
|
445
|
+
?, ?, ?, ?, datetime('now')
|
|
445
446
|
)
|
|
446
447
|
`);
|
|
447
448
|
stmt.run(
|
|
@@ -457,6 +458,7 @@ function saveAuthSession(accountId, session) {
|
|
|
457
458
|
session.version || null,
|
|
458
459
|
session.userId || null,
|
|
459
460
|
session.tokenExpiresAt || null,
|
|
461
|
+
session.refreshToken || null,
|
|
460
462
|
session.capturedAt ?? Date.now()
|
|
461
463
|
);
|
|
462
464
|
}
|
|
@@ -465,7 +467,7 @@ function getValidAuthSession(accountId, maxAgeMs = 30 * 24 * 60 * 60 * 1e3) {
|
|
|
465
467
|
const row = database.prepare(
|
|
466
468
|
`SELECT account_id, cookie, user_agent, bx_v, bx_ua, bx_umidtoken,
|
|
467
469
|
sec_ch_ua, sec_ch_ua_mobile, sec_ch_ua_platform, version,
|
|
468
|
-
user_id, token_expires_at, captured_at
|
|
470
|
+
user_id, token_expires_at, refresh_token, captured_at
|
|
469
471
|
FROM qwen_auth_sessions WHERE account_id = ?`
|
|
470
472
|
).get(accountId);
|
|
471
473
|
if (!row) return null;
|
|
@@ -473,7 +475,7 @@ function getValidAuthSession(accountId, maxAgeMs = 30 * 24 * 60 * 60 * 1e3) {
|
|
|
473
475
|
if (capturedAt <= 0 || Date.now() - capturedAt > maxAgeMs) {
|
|
474
476
|
return null;
|
|
475
477
|
}
|
|
476
|
-
if (row.token_expires_at) {
|
|
478
|
+
if (!row.refresh_token && row.token_expires_at) {
|
|
477
479
|
const tokenExpMs = Number(row.token_expires_at) * 1e3;
|
|
478
480
|
if (tokenExpMs <= Date.now() + 5 * 60 * 1e3) {
|
|
479
481
|
return null;
|
|
@@ -495,6 +497,7 @@ function getValidAuthSession(accountId, maxAgeMs = 30 * 24 * 60 * 60 * 1e3) {
|
|
|
495
497
|
version: row.version || void 0,
|
|
496
498
|
userId: row.user_id || void 0,
|
|
497
499
|
tokenExpiresAt: row.token_expires_at ? Number(row.token_expires_at) : void 0,
|
|
500
|
+
refreshToken: row.refresh_token || void 0,
|
|
498
501
|
capturedAt
|
|
499
502
|
};
|
|
500
503
|
}
|
|
@@ -2013,7 +2016,15 @@ function defaultCooldownDurationMs() {
|
|
|
2013
2016
|
return computeQuotaCooldownMs(Date.now());
|
|
2014
2017
|
}
|
|
2015
2018
|
function markAccountRateLimited(accountId, cooldownMs, reason, options = {}) {
|
|
2019
|
+
if (cooldownMs !== void 0 && cooldownMs <= 0) {
|
|
2020
|
+
clearAccountCooldown(accountId);
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2016
2023
|
const duration = cooldownMs ?? defaultCooldownDurationMs();
|
|
2024
|
+
if (duration <= 0) {
|
|
2025
|
+
clearAccountCooldown(accountId);
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2017
2028
|
const until = Date.now() + duration;
|
|
2018
2029
|
const cooldownReason = reason ?? "RateLimited";
|
|
2019
2030
|
cooldowns.set(accountId, {
|
|
@@ -3137,6 +3148,7 @@ __export(playwright_exports, {
|
|
|
3137
3148
|
parseCookiesForBrowser: () => parseCookiesForBrowser,
|
|
3138
3149
|
pruneAllPlaywrightProfiles: () => pruneAllPlaywrightProfiles,
|
|
3139
3150
|
prunePlaywrightProfileCaches: () => prunePlaywrightProfileCaches,
|
|
3151
|
+
refreshAccountToken: () => refreshAccountToken,
|
|
3140
3152
|
refreshHeaders: () => refreshHeaders,
|
|
3141
3153
|
refreshHeadersWithProfileReset: () => refreshHeadersWithProfileReset,
|
|
3142
3154
|
registerPlaywrightAccountForTests: () => registerPlaywrightAccountForTests,
|
|
@@ -4772,6 +4784,7 @@ async function loginToQwen(accountId, email, password) {
|
|
|
4772
4784
|
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || void 0,
|
|
4773
4785
|
version: cache.headers["version"] || void 0,
|
|
4774
4786
|
tokenExpiresAt: exp || void 0,
|
|
4787
|
+
refreshToken: apiResult.refreshToken,
|
|
4775
4788
|
capturedAt: Date.now()
|
|
4776
4789
|
});
|
|
4777
4790
|
}
|
|
@@ -4805,6 +4818,13 @@ async function loginToQwen(accountId, email, password) {
|
|
|
4805
4818
|
markAccountHeadersReady(accountId);
|
|
4806
4819
|
const { saveAuthSession: saveAuthSession2 } = await Promise.resolve().then(() => (init_database(), database_exports));
|
|
4807
4820
|
const exp = parseJwtExpiry(tokenCookie.value);
|
|
4821
|
+
let rTok = uiResult.refreshToken;
|
|
4822
|
+
if (!rTok) {
|
|
4823
|
+
try {
|
|
4824
|
+
rTok = await page.evaluate(() => localStorage.getItem("refresh_token") || "") || void 0;
|
|
4825
|
+
} catch {
|
|
4826
|
+
}
|
|
4827
|
+
}
|
|
4808
4828
|
saveAuthSession2(accountId, {
|
|
4809
4829
|
cookie: cookieStr,
|
|
4810
4830
|
userAgent: cache.headers["user-agent"] || "",
|
|
@@ -4816,6 +4836,7 @@ async function loginToQwen(accountId, email, password) {
|
|
|
4816
4836
|
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || void 0,
|
|
4817
4837
|
version: cache.headers["version"] || void 0,
|
|
4818
4838
|
tokenExpiresAt: exp || void 0,
|
|
4839
|
+
refreshToken: rTok,
|
|
4819
4840
|
capturedAt: Date.now()
|
|
4820
4841
|
});
|
|
4821
4842
|
}
|
|
@@ -4994,7 +5015,7 @@ async function loginViaApi(page, email, password) {
|
|
|
4994
5015
|
});
|
|
4995
5016
|
await page.reload({ waitUntil: "domcontentloaded" }).catch(() => {
|
|
4996
5017
|
});
|
|
4997
|
-
return { success: true };
|
|
5018
|
+
return { success: true, token, refreshToken };
|
|
4998
5019
|
}
|
|
4999
5020
|
}
|
|
5000
5021
|
return {
|
|
@@ -5237,6 +5258,137 @@ async function loginViaUi(page, email, password) {
|
|
|
5237
5258
|
return { success: false, reason: err?.message || String(err) };
|
|
5238
5259
|
}
|
|
5239
5260
|
}
|
|
5261
|
+
async function refreshAccountToken(accountId) {
|
|
5262
|
+
const page = accountPages.get(accountId);
|
|
5263
|
+
if (!page || page.isClosed()) {
|
|
5264
|
+
return { success: false, error: "Account page not active" };
|
|
5265
|
+
}
|
|
5266
|
+
const { getValidAuthSession: getValidAuthSession2, saveAuthSession: saveAuthSession2 } = await Promise.resolve().then(() => (init_database(), database_exports));
|
|
5267
|
+
const session = getValidAuthSession2(accountId);
|
|
5268
|
+
let refreshToken = session?.refreshToken;
|
|
5269
|
+
if (!refreshToken) {
|
|
5270
|
+
try {
|
|
5271
|
+
refreshToken = await page.evaluate(() => localStorage.getItem("refresh_token") || "") || void 0;
|
|
5272
|
+
} catch {
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
if (refreshToken) {
|
|
5276
|
+
try {
|
|
5277
|
+
const refreshResult = await page.evaluate(
|
|
5278
|
+
async ({ rTok }) => {
|
|
5279
|
+
try {
|
|
5280
|
+
const response = await fetch("https://auth.qwen.ai/api/v2/auths/refresh", {
|
|
5281
|
+
method: "GET",
|
|
5282
|
+
credentials: "include",
|
|
5283
|
+
headers: {
|
|
5284
|
+
accept: "application/json, text/plain, */*",
|
|
5285
|
+
source: "web",
|
|
5286
|
+
version: "0.3.11",
|
|
5287
|
+
"x-request-origin": "https://chat.qwen.ai",
|
|
5288
|
+
timezone: (/* @__PURE__ */ new Date()).toString().split(" (")[0],
|
|
5289
|
+
authorization: `Bearer ${rTok}`
|
|
5290
|
+
}
|
|
5291
|
+
});
|
|
5292
|
+
const json2 = await response.json().catch(() => null);
|
|
5293
|
+
return { ok: response.ok, status: response.status, data: json2 };
|
|
5294
|
+
} catch (e) {
|
|
5295
|
+
return { ok: false, error: e.message };
|
|
5296
|
+
}
|
|
5297
|
+
},
|
|
5298
|
+
{ rTok: refreshToken }
|
|
5299
|
+
);
|
|
5300
|
+
const json = refreshResult?.data;
|
|
5301
|
+
if (json && json.success === true && json.data) {
|
|
5302
|
+
const newAccessToken = json.data.access_token || json.data.token;
|
|
5303
|
+
const newRefreshToken = json.data.refresh_token || refreshToken;
|
|
5304
|
+
if (newAccessToken) {
|
|
5305
|
+
await page.context().addCookies([
|
|
5306
|
+
{
|
|
5307
|
+
name: "token",
|
|
5308
|
+
value: newAccessToken,
|
|
5309
|
+
domain: ".qwen.ai",
|
|
5310
|
+
path: "/",
|
|
5311
|
+
expires: Math.floor(Date.now() / 1e3) + 31536e3,
|
|
5312
|
+
httpOnly: false,
|
|
5313
|
+
secure: true,
|
|
5314
|
+
sameSite: "Lax"
|
|
5315
|
+
}
|
|
5316
|
+
]);
|
|
5317
|
+
await page.evaluate(
|
|
5318
|
+
({ tok, rTok }) => {
|
|
5319
|
+
try {
|
|
5320
|
+
localStorage.removeItem("qwen_token_logged_out_marker");
|
|
5321
|
+
localStorage.setItem("token", tok);
|
|
5322
|
+
localStorage.setItem("access_token", tok);
|
|
5323
|
+
if (rTok) localStorage.setItem("refresh_token", rTok);
|
|
5324
|
+
document.cookie = `token=${encodeURIComponent(tok)}; path=/; domain=.qwen.ai; max-age=31536000`;
|
|
5325
|
+
} catch {
|
|
5326
|
+
}
|
|
5327
|
+
},
|
|
5328
|
+
{ tok: newAccessToken, rTok: newRefreshToken }
|
|
5329
|
+
).catch(() => {
|
|
5330
|
+
});
|
|
5331
|
+
const liveCookies = await page.context().cookies();
|
|
5332
|
+
const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
5333
|
+
const cache = getHeaderCache(accountId);
|
|
5334
|
+
cache.headers.cookie = cookieStr;
|
|
5335
|
+
cache.lastRefresh = Date.now();
|
|
5336
|
+
markAccountHeadersReady(accountId);
|
|
5337
|
+
saveAuthSession2(accountId, {
|
|
5338
|
+
cookie: cookieStr,
|
|
5339
|
+
userAgent: cache.headers["user-agent"] || session?.userAgent || "",
|
|
5340
|
+
bxV: cache.headers["bx-v"] || session?.bxV || "2.5.37",
|
|
5341
|
+
bxUa: cache.headers["bx-ua"] || session?.bxUa || "",
|
|
5342
|
+
bxUmidtoken: cache.headers["bx-umidtoken"] || session?.bxUmidtoken || "",
|
|
5343
|
+
secChUa: cache.headers["sec-ch-ua"] || session?.secChUa,
|
|
5344
|
+
secChUaMobile: cache.headers["sec-ch-ua-mobile"] || session?.secChUaMobile,
|
|
5345
|
+
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || session?.secChUaPlatform,
|
|
5346
|
+
version: cache.headers["version"] || session?.version,
|
|
5347
|
+
userId: session?.userId,
|
|
5348
|
+
tokenExpiresAt: parseJwtExpiry(newAccessToken) || void 0,
|
|
5349
|
+
refreshToken: newRefreshToken,
|
|
5350
|
+
capturedAt: Date.now()
|
|
5351
|
+
});
|
|
5352
|
+
return { success: true, token: newAccessToken };
|
|
5353
|
+
}
|
|
5354
|
+
}
|
|
5355
|
+
} catch {
|
|
5356
|
+
}
|
|
5357
|
+
}
|
|
5358
|
+
const { getAccountCredentials: getAccountCredentials2 } = await Promise.resolve().then(() => (init_accounts(), accounts_exports));
|
|
5359
|
+
const creds = getAccountCredentials2(accountId);
|
|
5360
|
+
if (creds && creds.email && creds.password) {
|
|
5361
|
+
const apiResult = await loginViaApi(page, creds.email, creds.password);
|
|
5362
|
+
if (apiResult.success) {
|
|
5363
|
+
const liveCookies = await page.context().cookies();
|
|
5364
|
+
const tokenCookie = liveCookies.find((c) => c.name === "token");
|
|
5365
|
+
if (tokenCookie) {
|
|
5366
|
+
const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
5367
|
+
const cache = getHeaderCache(accountId);
|
|
5368
|
+
cache.headers.cookie = cookieStr;
|
|
5369
|
+
cache.lastRefresh = Date.now();
|
|
5370
|
+
markAccountHeadersReady(accountId);
|
|
5371
|
+
saveAuthSession2(accountId, {
|
|
5372
|
+
cookie: cookieStr,
|
|
5373
|
+
userAgent: cache.headers["user-agent"] || session?.userAgent || "",
|
|
5374
|
+
bxV: cache.headers["bx-v"] || session?.bxV || "2.5.37",
|
|
5375
|
+
bxUa: cache.headers["bx-ua"] || session?.bxUa || "",
|
|
5376
|
+
bxUmidtoken: cache.headers["bx-umidtoken"] || session?.bxUmidtoken || "",
|
|
5377
|
+
secChUa: cache.headers["sec-ch-ua"] || session?.secChUa,
|
|
5378
|
+
secChUaMobile: cache.headers["sec-ch-ua-mobile"] || session?.secChUaMobile,
|
|
5379
|
+
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || session?.secChUaPlatform,
|
|
5380
|
+
version: cache.headers["version"] || session?.version,
|
|
5381
|
+
userId: session?.userId,
|
|
5382
|
+
tokenExpiresAt: parseJwtExpiry(tokenCookie.value) || void 0,
|
|
5383
|
+
refreshToken: apiResult.refreshToken,
|
|
5384
|
+
capturedAt: Date.now()
|
|
5385
|
+
});
|
|
5386
|
+
return { success: true, token: tokenCookie.value };
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
return { success: false, error: "Could not refresh or re-authenticate token" };
|
|
5391
|
+
}
|
|
5240
5392
|
async function captureQwenHeaders(accountId, pageOverride, timeoutMs = config.timeouts.headers, triggerGraceMs = HEADER_CAPTURE_TRIGGER_GRACE_MS) {
|
|
5241
5393
|
const page = pageOverride ?? accountPages.get(accountId);
|
|
5242
5394
|
if (!page || page.isClosed()) {
|
|
@@ -5753,16 +5905,9 @@ async function refreshHeadersInternal(accountId, timeoutMs = config.timeouts.hea
|
|
|
5753
5905
|
}
|
|
5754
5906
|
};
|
|
5755
5907
|
if (forceReauth) {
|
|
5756
|
-
const
|
|
5757
|
-
if (
|
|
5758
|
-
|
|
5759
|
-
if (liveCookies.some((c) => c.name === "token")) {
|
|
5760
|
-
const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
5761
|
-
cache.headers.cookie = cookieStr;
|
|
5762
|
-
cache.lastRefresh = Date.now();
|
|
5763
|
-
markAccountHeadersReady(accountId);
|
|
5764
|
-
return;
|
|
5765
|
-
}
|
|
5908
|
+
const refreshed = await refreshAccountToken(accountId);
|
|
5909
|
+
if (refreshed.success) {
|
|
5910
|
+
return;
|
|
5766
5911
|
}
|
|
5767
5912
|
await executeReauth();
|
|
5768
5913
|
} else {
|
|
@@ -6198,50 +6343,15 @@ async function keepAlivePlaywrightAccount(accountId) {
|
|
|
6198
6343
|
const now = Date.now();
|
|
6199
6344
|
const currentUrl = page.url();
|
|
6200
6345
|
const cookie = await getCookies(accountId);
|
|
6201
|
-
if (cookie && isTokenExpiringSoon(cookie,
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
method: "GET",
|
|
6211
|
-
credentials: "include"
|
|
6212
|
-
}).catch(() => {
|
|
6213
|
-
});
|
|
6214
|
-
}).catch(() => {
|
|
6215
|
-
});
|
|
6216
|
-
const liveCookies = await page.context().cookies();
|
|
6217
|
-
const tokenCookie = liveCookies.find((c) => c.name === "token");
|
|
6218
|
-
if (tokenCookie) {
|
|
6219
|
-
const cookieStr = liveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
6220
|
-
const cache = getHeaderCache(accountId);
|
|
6221
|
-
cache.headers.cookie = cookieStr;
|
|
6222
|
-
cache.lastRefresh = Date.now();
|
|
6223
|
-
markAccountHeadersReady(accountId);
|
|
6224
|
-
lastKeepAliveNavigation.set(accountId, now);
|
|
6225
|
-
touchAccountActivity(accountId);
|
|
6226
|
-
return true;
|
|
6227
|
-
}
|
|
6228
|
-
} catch {
|
|
6229
|
-
}
|
|
6230
|
-
const loggedIn = await isPageLoggedIn(page, 2e3).catch(() => false);
|
|
6231
|
-
if (!loggedIn) {
|
|
6232
|
-
console.log(
|
|
6233
|
-
`\u{1F493} [SessionKeeper] Account ${accountId} session expired; renewing session...`
|
|
6234
|
-
);
|
|
6235
|
-
try {
|
|
6236
|
-
await refreshHeadersInternal(accountId, config.timeouts.headers, true);
|
|
6237
|
-
lastKeepAliveNavigation.set(accountId, now);
|
|
6238
|
-
touchAccountActivity(accountId);
|
|
6239
|
-
return true;
|
|
6240
|
-
} catch (err) {
|
|
6241
|
-
console.warn(
|
|
6242
|
-
`[SessionKeeper] Proactive session renewal failed for ${accountId}: ${getErrorMessage(err)}`
|
|
6243
|
-
);
|
|
6244
|
-
}
|
|
6346
|
+
if (cookie && isTokenExpiringSoon(cookie, 5)) {
|
|
6347
|
+
console.log(
|
|
6348
|
+
`\u{1F493} [SessionKeeper] Account ${accountId} access token expiring soon (<5m); silently refreshing...`
|
|
6349
|
+
);
|
|
6350
|
+
const res = await refreshAccountToken(accountId);
|
|
6351
|
+
if (res.success) {
|
|
6352
|
+
lastKeepAliveNavigation.set(accountId, now);
|
|
6353
|
+
touchAccountActivity(accountId);
|
|
6354
|
+
return true;
|
|
6245
6355
|
}
|
|
6246
6356
|
}
|
|
6247
6357
|
const lastNavigation = lastKeepAliveNavigation.get(accountId) ?? 0;
|
package/dist/reset-cooldowns.js
CHANGED
|
@@ -314,6 +314,7 @@ function runMigrations(db2) {
|
|
|
314
314
|
"sec_ch_ua_mobile TEXT",
|
|
315
315
|
"sec_ch_ua_platform TEXT",
|
|
316
316
|
"version TEXT",
|
|
317
|
+
"refresh_token TEXT",
|
|
317
318
|
"captured_at INTEGER DEFAULT 0"
|
|
318
319
|
];
|
|
319
320
|
for (const col of authSessionCols) {
|
|
@@ -392,11 +393,11 @@ function saveAuthSession(accountId, session) {
|
|
|
392
393
|
INSERT OR REPLACE INTO qwen_auth_sessions (
|
|
393
394
|
account_id, cookie, user_agent, bx_v, bx_ua, bx_umidtoken,
|
|
394
395
|
sec_ch_ua, sec_ch_ua_mobile, sec_ch_ua_platform, version,
|
|
395
|
-
user_id, token_expires_at, captured_at, updated_at
|
|
396
|
+
user_id, token_expires_at, refresh_token, captured_at, updated_at
|
|
396
397
|
) VALUES (
|
|
397
398
|
?, ?, ?, ?, ?, ?,
|
|
398
399
|
?, ?, ?, ?,
|
|
399
|
-
?, ?, ?, datetime('now')
|
|
400
|
+
?, ?, ?, ?, datetime('now')
|
|
400
401
|
)
|
|
401
402
|
`);
|
|
402
403
|
stmt.run(
|
|
@@ -412,6 +413,7 @@ function saveAuthSession(accountId, session) {
|
|
|
412
413
|
session.version || null,
|
|
413
414
|
session.userId || null,
|
|
414
415
|
session.tokenExpiresAt || null,
|
|
416
|
+
session.refreshToken || null,
|
|
415
417
|
session.capturedAt ?? Date.now()
|
|
416
418
|
);
|
|
417
419
|
}
|
|
@@ -420,7 +422,7 @@ function getValidAuthSession(accountId, maxAgeMs = 30 * 24 * 60 * 60 * 1e3) {
|
|
|
420
422
|
const row = database.prepare(
|
|
421
423
|
`SELECT account_id, cookie, user_agent, bx_v, bx_ua, bx_umidtoken,
|
|
422
424
|
sec_ch_ua, sec_ch_ua_mobile, sec_ch_ua_platform, version,
|
|
423
|
-
user_id, token_expires_at, captured_at
|
|
425
|
+
user_id, token_expires_at, refresh_token, captured_at
|
|
424
426
|
FROM qwen_auth_sessions WHERE account_id = ?`
|
|
425
427
|
).get(accountId);
|
|
426
428
|
if (!row) return null;
|
|
@@ -428,7 +430,7 @@ function getValidAuthSession(accountId, maxAgeMs = 30 * 24 * 60 * 60 * 1e3) {
|
|
|
428
430
|
if (capturedAt <= 0 || Date.now() - capturedAt > maxAgeMs) {
|
|
429
431
|
return null;
|
|
430
432
|
}
|
|
431
|
-
if (row.token_expires_at) {
|
|
433
|
+
if (!row.refresh_token && row.token_expires_at) {
|
|
432
434
|
const tokenExpMs = Number(row.token_expires_at) * 1e3;
|
|
433
435
|
if (tokenExpMs <= Date.now() + 5 * 60 * 1e3) {
|
|
434
436
|
return null;
|
|
@@ -450,6 +452,7 @@ function getValidAuthSession(accountId, maxAgeMs = 30 * 24 * 60 * 60 * 1e3) {
|
|
|
450
452
|
version: row.version || void 0,
|
|
451
453
|
userId: row.user_id || void 0,
|
|
452
454
|
tokenExpiresAt: row.token_expires_at ? Number(row.token_expires_at) : void 0,
|
|
455
|
+
refreshToken: row.refresh_token || void 0,
|
|
453
456
|
capturedAt
|
|
454
457
|
};
|
|
455
458
|
}
|
|
@@ -1107,7 +1110,15 @@ function defaultCooldownDurationMs() {
|
|
|
1107
1110
|
return computeQuotaCooldownMs(Date.now());
|
|
1108
1111
|
}
|
|
1109
1112
|
function markAccountRateLimited(accountId, cooldownMs, reason, options = {}) {
|
|
1113
|
+
if (cooldownMs !== void 0 && cooldownMs <= 0) {
|
|
1114
|
+
clearAccountCooldown(accountId);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1110
1117
|
const duration = cooldownMs ?? defaultCooldownDurationMs();
|
|
1118
|
+
if (duration <= 0) {
|
|
1119
|
+
clearAccountCooldown(accountId);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1111
1122
|
const until = Date.now() + duration;
|
|
1112
1123
|
const cooldownReason = reason ?? "RateLimited";
|
|
1113
1124
|
cooldowns.set(accountId, {
|
|
@@ -4048,6 +4059,7 @@ async function loginToQwen(accountId, email, password) {
|
|
|
4048
4059
|
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || void 0,
|
|
4049
4060
|
version: cache.headers["version"] || void 0,
|
|
4050
4061
|
tokenExpiresAt: exp || void 0,
|
|
4062
|
+
refreshToken: apiResult.refreshToken,
|
|
4051
4063
|
capturedAt: Date.now()
|
|
4052
4064
|
});
|
|
4053
4065
|
}
|
|
@@ -4081,6 +4093,13 @@ async function loginToQwen(accountId, email, password) {
|
|
|
4081
4093
|
markAccountHeadersReady(accountId);
|
|
4082
4094
|
const { saveAuthSession: saveAuthSession2 } = await Promise.resolve().then(() => (init_database(), database_exports));
|
|
4083
4095
|
const exp = parseJwtExpiry(tokenCookie.value);
|
|
4096
|
+
let rTok = uiResult.refreshToken;
|
|
4097
|
+
if (!rTok) {
|
|
4098
|
+
try {
|
|
4099
|
+
rTok = await page.evaluate(() => localStorage.getItem("refresh_token") || "") || void 0;
|
|
4100
|
+
} catch {
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4084
4103
|
saveAuthSession2(accountId, {
|
|
4085
4104
|
cookie: cookieStr,
|
|
4086
4105
|
userAgent: cache.headers["user-agent"] || "",
|
|
@@ -4092,6 +4111,7 @@ async function loginToQwen(accountId, email, password) {
|
|
|
4092
4111
|
secChUaPlatform: cache.headers["sec-ch-ua-platform"] || void 0,
|
|
4093
4112
|
version: cache.headers["version"] || void 0,
|
|
4094
4113
|
tokenExpiresAt: exp || void 0,
|
|
4114
|
+
refreshToken: rTok,
|
|
4095
4115
|
capturedAt: Date.now()
|
|
4096
4116
|
});
|
|
4097
4117
|
}
|
|
@@ -4270,7 +4290,7 @@ async function loginViaApi(page, email, password) {
|
|
|
4270
4290
|
});
|
|
4271
4291
|
await page.reload({ waitUntil: "domcontentloaded" }).catch(() => {
|
|
4272
4292
|
});
|
|
4273
|
-
return { success: true };
|
|
4293
|
+
return { success: true, token, refreshToken };
|
|
4274
4294
|
}
|
|
4275
4295
|
}
|
|
4276
4296
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.
|
|
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
|
|
package/src/core/database.ts
CHANGED
|
@@ -193,6 +193,7 @@ function runMigrations(db: Database.Database): void {
|
|
|
193
193
|
"sec_ch_ua_mobile TEXT",
|
|
194
194
|
"sec_ch_ua_platform TEXT",
|
|
195
195
|
"version TEXT",
|
|
196
|
+
"refresh_token TEXT",
|
|
196
197
|
"captured_at INTEGER DEFAULT 0",
|
|
197
198
|
];
|
|
198
199
|
for (const col of authSessionCols) {
|
|
@@ -309,6 +310,7 @@ export interface PersistedAuthSession {
|
|
|
309
310
|
version?: string;
|
|
310
311
|
userId?: string;
|
|
311
312
|
tokenExpiresAt?: number;
|
|
313
|
+
refreshToken?: string;
|
|
312
314
|
capturedAt: number;
|
|
313
315
|
}
|
|
314
316
|
|
|
@@ -326,6 +328,7 @@ export function saveAuthSession(
|
|
|
326
328
|
version?: string;
|
|
327
329
|
userId?: string;
|
|
328
330
|
tokenExpiresAt?: number;
|
|
331
|
+
refreshToken?: string;
|
|
329
332
|
capturedAt?: number;
|
|
330
333
|
},
|
|
331
334
|
): void {
|
|
@@ -334,11 +337,11 @@ export function saveAuthSession(
|
|
|
334
337
|
INSERT OR REPLACE INTO qwen_auth_sessions (
|
|
335
338
|
account_id, cookie, user_agent, bx_v, bx_ua, bx_umidtoken,
|
|
336
339
|
sec_ch_ua, sec_ch_ua_mobile, sec_ch_ua_platform, version,
|
|
337
|
-
user_id, token_expires_at, captured_at, updated_at
|
|
340
|
+
user_id, token_expires_at, refresh_token, captured_at, updated_at
|
|
338
341
|
) VALUES (
|
|
339
342
|
?, ?, ?, ?, ?, ?,
|
|
340
343
|
?, ?, ?, ?,
|
|
341
|
-
?, ?, ?, datetime('now')
|
|
344
|
+
?, ?, ?, ?, datetime('now')
|
|
342
345
|
)
|
|
343
346
|
`);
|
|
344
347
|
stmt.run(
|
|
@@ -354,6 +357,7 @@ export function saveAuthSession(
|
|
|
354
357
|
session.version || null,
|
|
355
358
|
session.userId || null,
|
|
356
359
|
session.tokenExpiresAt || null,
|
|
360
|
+
session.refreshToken || null,
|
|
357
361
|
session.capturedAt ?? Date.now(),
|
|
358
362
|
);
|
|
359
363
|
}
|
|
@@ -367,7 +371,7 @@ export function getValidAuthSession(
|
|
|
367
371
|
.prepare(
|
|
368
372
|
`SELECT account_id, cookie, user_agent, bx_v, bx_ua, bx_umidtoken,
|
|
369
373
|
sec_ch_ua, sec_ch_ua_mobile, sec_ch_ua_platform, version,
|
|
370
|
-
user_id, token_expires_at, captured_at
|
|
374
|
+
user_id, token_expires_at, refresh_token, captured_at
|
|
371
375
|
FROM qwen_auth_sessions WHERE account_id = ?`,
|
|
372
376
|
)
|
|
373
377
|
.get(accountId) as any;
|
|
@@ -379,9 +383,9 @@ export function getValidAuthSession(
|
|
|
379
383
|
return null;
|
|
380
384
|
}
|
|
381
385
|
|
|
382
|
-
if (row.token_expires_at) {
|
|
386
|
+
if (!row.refresh_token && row.token_expires_at) {
|
|
383
387
|
const tokenExpMs = Number(row.token_expires_at) * 1000;
|
|
384
|
-
// Safety margin of 5 minutes before token expires
|
|
388
|
+
// Safety margin of 5 minutes before token expires (only when no refresh_token exists)
|
|
385
389
|
if (tokenExpMs <= Date.now() + 5 * 60 * 1000) {
|
|
386
390
|
return null;
|
|
387
391
|
}
|
|
@@ -405,6 +409,7 @@ export function getValidAuthSession(
|
|
|
405
409
|
version: row.version || undefined,
|
|
406
410
|
userId: row.user_id || undefined,
|
|
407
411
|
tokenExpiresAt: row.token_expires_at ? Number(row.token_expires_at) : undefined,
|
|
412
|
+
refreshToken: row.refresh_token || undefined,
|
|
408
413
|
capturedAt,
|
|
409
414
|
};
|
|
410
415
|
}
|
|
@@ -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(
|
|
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();
|
package/src/core/version.ts
CHANGED
|
@@ -1681,15 +1681,27 @@ async function tryCreateStreamWithRetry(
|
|
|
1681
1681
|
}
|
|
1682
1682
|
|
|
1683
1683
|
if (policy.reason === "account_initialization_failed") {
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
currentAccountId
|
|
1690
|
-
|
|
1691
|
-
|
|
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
|
|
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 === "
|
|
150
|
+
return typeof phase === "string" && THINKING_PHASES.has(phase);
|
|
123
151
|
}
|
|
124
152
|
|
|
125
153
|
/**
|