qwenproxy-cli 1.6.10 → 1.6.12
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 +390 -30
- package/dist/delete-chats.js +946 -445
- package/dist/index.js +612 -100
- package/dist/login.js +417 -51
- package/dist/reset-cooldowns.js +769 -407
- package/dist/sync-clients.js +4 -1
- package/package.json +2 -2
- package/src/api/server.ts +8 -4
- package/src/core/config.ts +4 -1
- package/src/core/model-alias.ts +1 -0
- package/src/core/version.ts +1 -1
- package/src/core/watchdog.ts +4 -3
- package/src/routes/chat/account.ts +12 -0
- package/src/routes/chat/streaming.ts +18 -2
- package/src/services/auth-playwright.ts +3 -2
- package/src/services/captcha-coordinator.ts +41 -11
- package/src/services/captcha-solver.ts +400 -2
- package/src/services/chat-cleanup.ts +11 -2
- package/src/services/playwright.ts +135 -50
- package/src/services/qwen-chat-pool.ts +52 -9
- package/src/services/qwen-headers.ts +18 -1
- package/src/services/qwen.ts +35 -7
- package/src/services/session-keeper.ts +70 -8
|
@@ -25,6 +25,11 @@ const CAPTCHA_EVENT_EMOJI: Record<string, string> = {
|
|
|
25
25
|
recovery_succeeded: "✅",
|
|
26
26
|
recovery_not_solved: "⚠️",
|
|
27
27
|
recovery_failed: "❌",
|
|
28
|
+
aliyun_detected: "🧩",
|
|
29
|
+
aliyun_offset_failed: "📐",
|
|
30
|
+
aliyun_geometry: "📐",
|
|
31
|
+
aliyun_solve_succeeded: "✅",
|
|
32
|
+
aliyun_solve_failed: "❌",
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
const captchaDebugEnabled = process.env.CAPTCHA_DEBUG === "true";
|
|
@@ -61,6 +66,7 @@ const BAXIA_IFRAME_SELECTORS = [
|
|
|
61
66
|
`${BAXIA_CONTENT_SELECTOR} iframe`,
|
|
62
67
|
"iframe#baxia-dialog-content",
|
|
63
68
|
'iframe[src*="_____tmd_____/punish"]',
|
|
69
|
+
'iframe[src*="nocaptcha"]',
|
|
64
70
|
] as const;
|
|
65
71
|
|
|
66
72
|
const BAXIA_DOCUMENT_SELECTORS = [
|
|
@@ -77,9 +83,9 @@ const BAXIA_DOCUMENT_SELECTORS = [
|
|
|
77
83
|
export const BAXIA_IFRAME_SELECTOR = BAXIA_IFRAME_SELECTORS.join(", ");
|
|
78
84
|
|
|
79
85
|
const BAXIA_SLIDER_SELECTOR =
|
|
80
|
-
"#nc_1_n1z, .nc_1_n1z, .btn_slide, .nc_wrapper .btn_slide, ._nc .btn_slide, .nc-container .btn_slide";
|
|
86
|
+
"#nc_1_n1z, .nc_1_n1z, .btn_slide, .nc_iconfont.btn_slide, span[id*=\"_n1z\"], .nc_wrapper .btn_slide, ._nc .btn_slide, .nc-container .btn_slide";
|
|
81
87
|
const BAXIA_TRACK_SELECTOR =
|
|
82
|
-
"#nc_1_n1t, .nc_scale, .nc_wrapper .nc_scale, ._nc .nc_scale, .nc-container .nc_scale";
|
|
88
|
+
"#nc_1_n1t, .nc_scale, span[id*=\"_n1t\"], .nc_wrapper .nc_scale, ._nc .nc_scale, .nc-container .nc_scale";
|
|
83
89
|
const BAXIA_SUCCESS_SELECTOR =
|
|
84
90
|
".btn_ok, .nc_ok, .nc_success, .nc_result, .nc_wrapper.nc-success, .nc_wrapper.success, [data-nc-lang=\"SUCCESS\"], [data-nc-lang=\"success\"], #nc-loading-circle";
|
|
85
91
|
const BAXIA_RELOAD_SELECTOR =
|
|
@@ -153,6 +159,398 @@ const DEFAULT_RETRY_DELAY_MS = 1_000;
|
|
|
153
159
|
const DEFAULT_SETTLE_MS = 2_000;
|
|
154
160
|
const DEFAULT_SLIDER_TIMEOUT_MS = 8_000;
|
|
155
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Aliyun WAF jigsaw challenge used by the sign-in page. It is a separate
|
|
164
|
+
* widget from the Baxia/TMD NC dialog: an embedded block (not an iframe) with
|
|
165
|
+
* a background image, a transparent puzzle piece, and its own slider track.
|
|
166
|
+
*/
|
|
167
|
+
export const ALIYUN_WAF_CAPTCHA_SELECTOR = "#waf_nc_block";
|
|
168
|
+
export const ALIYUN_WAF_CAPTCHA_WIDGET = "#aliyunCaptcha-window-embed";
|
|
169
|
+
const ALIYUN_WAF_BLOCK_SELECTORS = [
|
|
170
|
+
ALIYUN_WAF_CAPTCHA_SELECTOR,
|
|
171
|
+
ALIYUN_WAF_CAPTCHA_WIDGET,
|
|
172
|
+
] as const;
|
|
173
|
+
const ALIYUN_PUZZLE_BACKGROUND_SELECTOR = "#aliyunCaptcha-img";
|
|
174
|
+
const ALIYUN_PUZZLE_PIECE_SELECTOR = "#aliyunCaptcha-puzzle";
|
|
175
|
+
const ALIYUN_PUZZLE_SLIDER_SELECTOR = "#aliyunCaptcha-sliding-slider";
|
|
176
|
+
const ALIYUN_PUZZLE_TRACK_SELECTOR = "#aliyunCaptcha-sliding-body";
|
|
177
|
+
|
|
178
|
+
interface AliyunPuzzleOffset {
|
|
179
|
+
ok: boolean;
|
|
180
|
+
offsetX?: number;
|
|
181
|
+
naturalWidth?: number;
|
|
182
|
+
reason?: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function findVisibleLocatorWithin(
|
|
186
|
+
page: Page,
|
|
187
|
+
selectors: readonly string[],
|
|
188
|
+
timeoutMs: number,
|
|
189
|
+
): Promise<{ selector: string; locator: Locator } | null> {
|
|
190
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
191
|
+
while (true) {
|
|
192
|
+
const found = await findVisibleLocator(page, selectors);
|
|
193
|
+
if (found) return found;
|
|
194
|
+
if (Date.now() >= deadline) return null;
|
|
195
|
+
await sleep(Math.min(150, Math.max(1, deadline - Date.now())));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function waitForHidden(locator: Locator, timeoutMs: number): Promise<boolean> {
|
|
200
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
201
|
+
while (true) {
|
|
202
|
+
if (!(await isVisible(locator))) return true;
|
|
203
|
+
if (Date.now() >= deadline) return false;
|
|
204
|
+
await sleep(Math.min(100, Math.max(1, deadline - Date.now())));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Locate the hole in the background image by scoring the vertical edges of
|
|
210
|
+
* the puzzle-piece silhouette against the background gradients. Returns the
|
|
211
|
+
* horizontal offset in the image's natural coordinate space.
|
|
212
|
+
*/
|
|
213
|
+
async function computeAliyunPuzzleOffset(
|
|
214
|
+
page: Page,
|
|
215
|
+
): Promise<AliyunPuzzleOffset> {
|
|
216
|
+
return page.evaluate((): Promise<AliyunPuzzleOffset> => {
|
|
217
|
+
const background = document.querySelector(
|
|
218
|
+
"#aliyunCaptcha-img",
|
|
219
|
+
) as HTMLImageElement | null;
|
|
220
|
+
const piece = document.querySelector(
|
|
221
|
+
"#aliyunCaptcha-puzzle",
|
|
222
|
+
) as HTMLImageElement | null;
|
|
223
|
+
if (!background || !piece) {
|
|
224
|
+
return Promise.resolve({ ok: false, reason: "missing puzzle images" });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const compute = (): AliyunPuzzleOffset => {
|
|
228
|
+
try {
|
|
229
|
+
const width = background.naturalWidth;
|
|
230
|
+
const height = background.naturalHeight;
|
|
231
|
+
const pieceWidth = piece.naturalWidth;
|
|
232
|
+
const pieceHeight = piece.naturalHeight;
|
|
233
|
+
if (!width || !height || !pieceWidth || !pieceHeight) {
|
|
234
|
+
return { ok: false, reason: "image dimensions unavailable" };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const backgroundCanvas = document.createElement("canvas");
|
|
238
|
+
// The anti-fingerprinting getImageData noise must not touch the
|
|
239
|
+
// solver's own canvas: a ±1 pixel shift corrupts edge scoring.
|
|
240
|
+
backgroundCanvas.id = "captcha-solver-canvas";
|
|
241
|
+
backgroundCanvas.width = width;
|
|
242
|
+
backgroundCanvas.height = height;
|
|
243
|
+
const backgroundContext = backgroundCanvas.getContext("2d");
|
|
244
|
+
if (!backgroundContext) {
|
|
245
|
+
return { ok: false, reason: "no background context" };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const pieceCanvas = document.createElement("canvas");
|
|
249
|
+
pieceCanvas.id = "captcha-solver-canvas";
|
|
250
|
+
pieceCanvas.width = pieceWidth;
|
|
251
|
+
pieceCanvas.height = pieceHeight;
|
|
252
|
+
const pieceContext = pieceCanvas.getContext("2d");
|
|
253
|
+
if (!pieceContext) {
|
|
254
|
+
return { ok: false, reason: "no piece context" };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
backgroundContext.drawImage(background, 0, 0);
|
|
259
|
+
} catch {
|
|
260
|
+
return { ok: false, reason: "background canvas tainted" };
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
pieceContext.drawImage(piece, 0, 0);
|
|
264
|
+
} catch {
|
|
265
|
+
return { ok: false, reason: "puzzle canvas tainted" };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const backgroundData = backgroundContext.getImageData(
|
|
269
|
+
0,
|
|
270
|
+
0,
|
|
271
|
+
width,
|
|
272
|
+
height,
|
|
273
|
+
).data;
|
|
274
|
+
const pieceData = pieceContext.getImageData(
|
|
275
|
+
0,
|
|
276
|
+
0,
|
|
277
|
+
pieceWidth,
|
|
278
|
+
pieceHeight,
|
|
279
|
+
).data;
|
|
280
|
+
|
|
281
|
+
let pieceMinX = pieceWidth;
|
|
282
|
+
let pieceMaxX = -1;
|
|
283
|
+
let pieceMinY = pieceHeight;
|
|
284
|
+
let pieceMaxY = -1;
|
|
285
|
+
for (let y = 0; y < pieceHeight; y++) {
|
|
286
|
+
for (let x = 0; x < pieceWidth; x++) {
|
|
287
|
+
if (pieceData[(y * pieceWidth + x) * 4 + 3] > 100) {
|
|
288
|
+
if (x < pieceMinX) pieceMinX = x;
|
|
289
|
+
if (x > pieceMaxX) pieceMaxX = x;
|
|
290
|
+
if (y < pieceMinY) pieceMinY = y;
|
|
291
|
+
if (y > pieceMaxY) pieceMaxY = y;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const puzzleWidth = pieceMaxX - pieceMinX + 1;
|
|
296
|
+
if (puzzleWidth <= 0) {
|
|
297
|
+
return { ok: false, reason: "empty puzzle piece" };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const yStart = Math.max(1, Math.min(height - 2, pieceMinY + 5));
|
|
301
|
+
const yEnd = Math.max(
|
|
302
|
+
yStart,
|
|
303
|
+
Math.min(height - 2, pieceMaxY - 5),
|
|
304
|
+
);
|
|
305
|
+
const columnGradients = new Float32Array(width);
|
|
306
|
+
for (let x = 1; x < width - 1; x++) {
|
|
307
|
+
let gradient = 0;
|
|
308
|
+
for (let y = yStart; y <= yEnd; y++) {
|
|
309
|
+
const left = (y * width + x - 1) * 4;
|
|
310
|
+
const right = (y * width + x + 1) * 4;
|
|
311
|
+
gradient +=
|
|
312
|
+
Math.abs(backgroundData[right] - backgroundData[left]) +
|
|
313
|
+
Math.abs(backgroundData[right + 1] - backgroundData[left + 1]) +
|
|
314
|
+
Math.abs(backgroundData[right + 2] - backgroundData[left + 2]);
|
|
315
|
+
}
|
|
316
|
+
columnGradients[x] = gradient;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let bestX = 0;
|
|
320
|
+
let bestScore = 0;
|
|
321
|
+
const minCandidateX = 30;
|
|
322
|
+
const maxCandidateX = width - puzzleWidth - 20;
|
|
323
|
+
for (
|
|
324
|
+
let candidateX = minCandidateX;
|
|
325
|
+
candidateX <= maxCandidateX;
|
|
326
|
+
candidateX++
|
|
327
|
+
) {
|
|
328
|
+
let leftMax = 0;
|
|
329
|
+
for (let dx = -2; dx <= 2; dx++) {
|
|
330
|
+
const value = columnGradients[candidateX + pieceMinX + dx] || 0;
|
|
331
|
+
if (value > leftMax) leftMax = value;
|
|
332
|
+
}
|
|
333
|
+
let rightMax = 0;
|
|
334
|
+
for (let dx = -3; dx <= 1; dx++) {
|
|
335
|
+
const value = columnGradients[candidateX + pieceMaxX + dx] || 0;
|
|
336
|
+
if (value > rightMax) rightMax = value;
|
|
337
|
+
}
|
|
338
|
+
const score = Math.sqrt(leftMax * rightMax);
|
|
339
|
+
if (score > bestScore) {
|
|
340
|
+
bestScore = score;
|
|
341
|
+
bestX = candidateX;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (bestScore <= 0) {
|
|
346
|
+
return { ok: false, reason: "no matching hole edge" };
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
ok: true,
|
|
350
|
+
offsetX: bestX - pieceMinX,
|
|
351
|
+
naturalWidth: width,
|
|
352
|
+
};
|
|
353
|
+
} catch (error) {
|
|
354
|
+
return {
|
|
355
|
+
ok: false,
|
|
356
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
return new Promise<AliyunPuzzleOffset>((resolve) => {
|
|
362
|
+
if (background.complete && piece.complete) {
|
|
363
|
+
resolve(compute());
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
let settled = false;
|
|
367
|
+
const finish = () => {
|
|
368
|
+
if (settled) return;
|
|
369
|
+
settled = true;
|
|
370
|
+
resolve(compute());
|
|
371
|
+
};
|
|
372
|
+
const fail = () => {
|
|
373
|
+
if (settled) return;
|
|
374
|
+
settled = true;
|
|
375
|
+
resolve({ ok: false, reason: "puzzle image load error" });
|
|
376
|
+
};
|
|
377
|
+
background.addEventListener("load", finish, { once: true });
|
|
378
|
+
piece.addEventListener("load", finish, { once: true });
|
|
379
|
+
background.addEventListener("error", fail, { once: true });
|
|
380
|
+
piece.addEventListener("error", fail, { once: true });
|
|
381
|
+
setTimeout(() => {
|
|
382
|
+
if (settled) return;
|
|
383
|
+
settled = true;
|
|
384
|
+
resolve({ ok: false, reason: "puzzle image load timeout" });
|
|
385
|
+
}, 2_000);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export interface AliyunWafSolverOptions {
|
|
391
|
+
maxAttempts?: number;
|
|
392
|
+
waitForMs?: number;
|
|
393
|
+
retryDelayMs?: number;
|
|
394
|
+
settleMs?: number;
|
|
395
|
+
sliderTimeoutMs?: number;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Solve the Aliyun WAF jigsaw challenge rendered on the sign-in page. The
|
|
400
|
+
* widget is not inside an iframe and its puzzle geometry comes from the two
|
|
401
|
+
* server-supplied images, so the solver locates the hole by image analysis
|
|
402
|
+
* before driving the slider with the same human-like drag as Baxia.
|
|
403
|
+
*/
|
|
404
|
+
export async function solveAliyunWafCaptcha(
|
|
405
|
+
page: Page,
|
|
406
|
+
options: AliyunWafSolverOptions = {},
|
|
407
|
+
): Promise<boolean> {
|
|
408
|
+
const waitForMs = Math.max(0, options.waitForMs ?? 0);
|
|
409
|
+
const maxAttempts = Math.max(
|
|
410
|
+
1,
|
|
411
|
+
Math.min(5, options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS),
|
|
412
|
+
);
|
|
413
|
+
const retryDelayMs = Math.max(
|
|
414
|
+
0,
|
|
415
|
+
options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS,
|
|
416
|
+
);
|
|
417
|
+
const settleMs = Math.max(0, options.settleMs ?? DEFAULT_SETTLE_MS);
|
|
418
|
+
const sliderTimeoutMs = Math.max(
|
|
419
|
+
500,
|
|
420
|
+
options.sliderTimeoutMs ?? DEFAULT_SLIDER_TIMEOUT_MS,
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const detected = await findVisibleLocatorWithin(
|
|
424
|
+
page,
|
|
425
|
+
ALIYUN_WAF_BLOCK_SELECTORS,
|
|
426
|
+
waitForMs,
|
|
427
|
+
);
|
|
428
|
+
if (!detected) return false;
|
|
429
|
+
|
|
430
|
+
// The same physical-pointer rule as Baxia: two solvers must never
|
|
431
|
+
// interleave drag events on one browser.
|
|
432
|
+
return withCaptchaMouseLock(async () => {
|
|
433
|
+
const block = detected.locator;
|
|
434
|
+
if (!(await isVisible(block))) return false;
|
|
435
|
+
logBaxiaCaptcha("aliyun_detected", { selector: detected.selector });
|
|
436
|
+
|
|
437
|
+
let lastReason = "unknown";
|
|
438
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
439
|
+
try {
|
|
440
|
+
const offset = await computeAliyunPuzzleOffset(page);
|
|
441
|
+
if (
|
|
442
|
+
!offset.ok ||
|
|
443
|
+
typeof offset.offsetX !== "number" ||
|
|
444
|
+
typeof offset.naturalWidth !== "number" ||
|
|
445
|
+
offset.naturalWidth <= 0
|
|
446
|
+
) {
|
|
447
|
+
lastReason = offset.reason || "offset_unavailable";
|
|
448
|
+
logBaxiaCaptcha("aliyun_offset_failed", {
|
|
449
|
+
attempt,
|
|
450
|
+
reason: lastReason,
|
|
451
|
+
});
|
|
452
|
+
} else {
|
|
453
|
+
const slider = page.locator(ALIYUN_PUZZLE_SLIDER_SELECTOR).first();
|
|
454
|
+
const track = page.locator(ALIYUN_PUZZLE_TRACK_SELECTOR).first();
|
|
455
|
+
await slider.waitFor({ state: "visible", timeout: sliderTimeoutMs });
|
|
456
|
+
const [sliderBox, trackBox] = await Promise.all([
|
|
457
|
+
slider.boundingBox(),
|
|
458
|
+
track.boundingBox(),
|
|
459
|
+
]);
|
|
460
|
+
if (!sliderBox || !trackBox || trackBox.width <= 0) {
|
|
461
|
+
lastReason = "bounds_unavailable";
|
|
462
|
+
logBaxiaCaptcha("aliyun_offset_failed", {
|
|
463
|
+
attempt,
|
|
464
|
+
reason: lastReason,
|
|
465
|
+
});
|
|
466
|
+
} else {
|
|
467
|
+
const scale = trackBox.width / offset.naturalWidth;
|
|
468
|
+
const dragDistance = Math.round(offset.offsetX * scale);
|
|
469
|
+
logBaxiaCaptcha("aliyun_geometry", {
|
|
470
|
+
attempt,
|
|
471
|
+
offset: offset.offsetX,
|
|
472
|
+
scale: Number(scale.toFixed(2)),
|
|
473
|
+
distance: dragDistance,
|
|
474
|
+
});
|
|
475
|
+
if (dragDistance <= 0) {
|
|
476
|
+
lastReason = "non_positive_distance";
|
|
477
|
+
} else {
|
|
478
|
+
const startX = sliderBox.x + sliderBox.width / 2;
|
|
479
|
+
const startY = sliderBox.y + sliderBox.height / 2;
|
|
480
|
+
await humanDrag(
|
|
481
|
+
page,
|
|
482
|
+
startX,
|
|
483
|
+
startY,
|
|
484
|
+
startX + dragDistance,
|
|
485
|
+
startY,
|
|
486
|
+
);
|
|
487
|
+
if (await waitForHidden(block, settleMs)) {
|
|
488
|
+
logBaxiaCaptcha(
|
|
489
|
+
"aliyun_solve_succeeded",
|
|
490
|
+
{ attempt, distance: dragDistance },
|
|
491
|
+
true,
|
|
492
|
+
);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
lastReason = "not_solved";
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
} catch (error) {
|
|
500
|
+
lastReason =
|
|
501
|
+
error instanceof Error ? error.name : "UnknownError";
|
|
502
|
+
logBaxiaCaptcha("aliyun_offset_failed", {
|
|
503
|
+
attempt,
|
|
504
|
+
error: lastReason,
|
|
505
|
+
detail: sanitizeCaptchaErrorDetail(error),
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (attempt < maxAttempts && retryDelayMs > 0) {
|
|
510
|
+
await sleep(retryDelayMs);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
logBaxiaCaptcha(
|
|
515
|
+
"aliyun_solve_failed",
|
|
516
|
+
{ attempts: maxAttempts, reason: lastReason },
|
|
517
|
+
true,
|
|
518
|
+
);
|
|
519
|
+
return false;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Aliyun's widget lives only on the auth page and renders quickly; probing it
|
|
524
|
+
* for the whole Baxia budget would leave the NC dialog unsolved. */
|
|
525
|
+
const ALIYUN_PROBE_MAX_WAIT_MS = 2_000;
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Solve whichever first-party challenge is currently rendered. Called by the
|
|
529
|
+
* login flow (Aliyun jigsaw) and the chat helpers (Baxia NC).
|
|
530
|
+
*/
|
|
531
|
+
export async function solveVisibleCaptcha(
|
|
532
|
+
page: Page,
|
|
533
|
+
options: BaxiaSolverOptions = {},
|
|
534
|
+
): Promise<boolean> {
|
|
535
|
+
if (typeof page.isClosed === "function" && page.isClosed()) return false;
|
|
536
|
+
|
|
537
|
+
// Split the wait budget between the two solvers instead of letting the
|
|
538
|
+
// Aliyun probe (absent on chat pages) consume all of it.
|
|
539
|
+
const totalWaitMs = Math.max(0, options.waitForMs ?? 0);
|
|
540
|
+
const aliyunWaitMs = Math.min(totalWaitMs, ALIYUN_PROBE_MAX_WAIT_MS);
|
|
541
|
+
const startedAt = Date.now();
|
|
542
|
+
if (
|
|
543
|
+
await solveAliyunWafCaptcha(page, { ...options, waitForMs: aliyunWaitMs })
|
|
544
|
+
) {
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
const remainingWaitMs = Math.max(
|
|
548
|
+
0,
|
|
549
|
+
totalWaitMs - (Date.now() - startedAt),
|
|
550
|
+
);
|
|
551
|
+
return solveBaxiaCaptcha(page, { ...options, waitForMs: remainingWaitMs });
|
|
552
|
+
}
|
|
553
|
+
|
|
156
554
|
async function isVisible(locator: Locator): Promise<boolean> {
|
|
157
555
|
return locator.isVisible().catch(() => false);
|
|
158
556
|
}
|
|
@@ -79,7 +79,15 @@ export async function deleteChatsForAccountId(accountId: string): Promise<boolea
|
|
|
79
79
|
if (!credentials) {
|
|
80
80
|
throw new Error(`Credenciais da conta ${account.email} não encontradas.`);
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
const wasInitialized = isPlaywrightInitialized(account.id);
|
|
83
|
+
try {
|
|
84
|
+
return await deleteChatsForAccount(credentials);
|
|
85
|
+
} finally {
|
|
86
|
+
if (!wasInitialized && !isAuthMockEnabled()) {
|
|
87
|
+
const { closePlaywrightForAccount } = await import("./playwright.ts");
|
|
88
|
+
await closePlaywrightForAccount(account.id).catch(() => {});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false): Promise<DeleteChatsResult> {
|
|
@@ -100,6 +108,7 @@ export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false):
|
|
|
100
108
|
const account = accounts[i];
|
|
101
109
|
const currentIdx = i + 1;
|
|
102
110
|
const totalCount = accounts.length;
|
|
111
|
+
const wasInitialized = isPlaywrightInitialized(account.id);
|
|
103
112
|
try {
|
|
104
113
|
const ok = await deleteChatsForAccount(account, currentIdx, totalCount);
|
|
105
114
|
if (ok) succeeded++;
|
|
@@ -109,7 +118,7 @@ export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false):
|
|
|
109
118
|
error instanceof Error ? error.message : String(error),
|
|
110
119
|
);
|
|
111
120
|
} finally {
|
|
112
|
-
if (!keepBrowserOpen) {
|
|
121
|
+
if ((!keepBrowserOpen || !wasInitialized) && !isAuthMockEnabled()) {
|
|
113
122
|
const { closePlaywrightForAccount } = await import("./playwright.ts");
|
|
114
123
|
await closePlaywrightForAccount(account.id).catch(() => {});
|
|
115
124
|
}
|