Fix xianyu QR login refresh
Exclude tiny QR-login icon candidates, expose captured QR dimensions in the Web UI, and refresh the portable Hermes skill release archives.
This commit is contained in:
@@ -141,27 +141,53 @@ async function clickIfVisible(page, selector, timeout = 1000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function isUsableQrCandidate(loc) {
|
||||
try {
|
||||
const box = await loc.boundingBox({ timeout: 1200 });
|
||||
if (!box) return false;
|
||||
// Goofish/Taobao pages contain tiny QR icons (for switching login modes).
|
||||
// Those are visible canvases/images too, but screenshotting them produces a
|
||||
// 24x24 icon instead of the actual scannable QR code. Only accept elements
|
||||
// large enough to be a real QR/QR container.
|
||||
return box.width >= 120 && box.height >= 120;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function findQrLikeLocator(page) {
|
||||
const selectors = [
|
||||
'canvas',
|
||||
'img[src^="data:image"]',
|
||||
'img[src*="qr"]',
|
||||
'img[src*="qrcode"]',
|
||||
'[class*="qrcode"] canvas',
|
||||
'[class*="qrcode"] img',
|
||||
'[class*="qr-code"] canvas',
|
||||
'[class*="qr-code"] img',
|
||||
'[class*="qrCode"] canvas',
|
||||
'[class*="qrCode"] img',
|
||||
'[class*="qr"] canvas',
|
||||
'[class*="qr"] img',
|
||||
'[class*="login"] canvas',
|
||||
'[class*="login"] img',
|
||||
'canvas',
|
||||
'img[src^="data:image"]',
|
||||
'img[src*="qr"]',
|
||||
'img[src*="qrcode"]',
|
||||
];
|
||||
for (const selector of selectors) {
|
||||
const loc = page.locator(selector).first();
|
||||
if (await loc.isVisible({ timeout: 1200 }).catch(() => false)) return loc;
|
||||
const locs = page.locator(selector);
|
||||
const count = Math.min(await locs.count().catch(() => 0), 8);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const loc = locs.nth(i);
|
||||
if (await loc.isVisible({ timeout: 800 }).catch(() => false) && await isUsableQrCandidate(loc)) return loc;
|
||||
}
|
||||
}
|
||||
for (const frame of page.frames()) {
|
||||
for (const selector of selectors) {
|
||||
const loc = frame.locator(selector).first();
|
||||
if (await loc.isVisible({ timeout: 800 }).catch(() => false)) return loc;
|
||||
const locs = frame.locator(selector);
|
||||
const count = Math.min(await locs.count().catch(() => 0), 8);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const loc = locs.nth(i);
|
||||
if (await loc.isVisible({ timeout: 500 }).catch(() => false) && await isUsableQrCandidate(loc)) return loc;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -197,16 +223,50 @@ export async function getLoginQr(emitLog = () => {}) {
|
||||
let screenshotKind = '';
|
||||
if (!status.loggedIn) {
|
||||
const qrLike = await findQrLikeLocator(page);
|
||||
const modal = page.locator('[class*="login-modal"], [class*="Login"], [class*="qrcode"], [class*="qr"], iframe').first();
|
||||
const target = qrLike || (await modal.isVisible({ timeout: 1500 }).catch(() => false) ? modal : page.locator('body'));
|
||||
screenshotKind = qrLike ? '二维码区域' : '登录面板截图';
|
||||
const modalCandidates = [
|
||||
'[class*="login-modal"]',
|
||||
'[class*="Login"]',
|
||||
'[class*="qrcode"]',
|
||||
'[class*="qr-code"]',
|
||||
'[class*="qrCode"]',
|
||||
'[class*="qr"]',
|
||||
'iframe',
|
||||
];
|
||||
let modal = null;
|
||||
for (const selector of modalCandidates) {
|
||||
const locs = page.locator(selector);
|
||||
const count = Math.min(await locs.count().catch(() => 0), 8);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const loc = locs.nth(i);
|
||||
if (await loc.isVisible({ timeout: 500 }).catch(() => false) && await isUsableQrCandidate(loc)) {
|
||||
modal = loc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (modal) break;
|
||||
}
|
||||
const target = qrLike || modal || page.locator('body');
|
||||
screenshotKind = qrLike ? '二维码区域' : (modal ? '登录面板截图' : '当前登录页面截图');
|
||||
const buf = await target.screenshot({ type: 'png', timeout: 8000 }).catch(() => null);
|
||||
if (buf) qrImage = `data:image/png;base64,${buf.toString('base64')}`;
|
||||
}
|
||||
|
||||
let qrImageWidth = 0;
|
||||
let qrImageHeight = 0;
|
||||
if (qrImage) {
|
||||
const match = qrImage.match(/^data:image\/png;base64,(.+)$/);
|
||||
if (match) {
|
||||
const raw = Buffer.from(match[1], 'base64');
|
||||
if (raw.length >= 24 && raw.toString('ascii', 1, 4) === 'PNG') {
|
||||
qrImageWidth = raw.readUInt32BE(16);
|
||||
qrImageHeight = raw.readUInt32BE(20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (status.loggedIn) emitLog('✅ 当前账号已登录,无需扫码');
|
||||
else emitLog(`已从${redirectedProvider}生成${screenshotKind || '登录截图'}${clicked ? '' : '(未找到明确登录按钮,已截取当前页面)'};请扫码后点“刷新账号状态”。当前 URL:${currentUrl}`);
|
||||
return { ...status, qrImage, qrCheckedAt: new Date().toISOString(), qrSource: redirectedProvider, qrUrl: currentUrl, qrScreenshotKind: screenshotKind };
|
||||
else emitLog(`已从${redirectedProvider}生成${screenshotKind || '登录截图'}${qrImageWidth && qrImageHeight ? `(${qrImageWidth}×${qrImageHeight})` : ''}${clicked ? '' : '(未找到明确登录按钮,已截取当前页面)'};请扫码后点“刷新账号状态”。当前 URL:${currentUrl}`);
|
||||
return { ...status, qrImage, qrCheckedAt: new Date().toISOString(), qrSource: redirectedProvider, qrUrl: currentUrl, qrScreenshotKind: screenshotKind, qrImageWidth, qrImageHeight };
|
||||
}
|
||||
|
||||
export async function getLoginStatus(existingPage = null) {
|
||||
|
||||
@@ -240,7 +240,8 @@ function renderLoginQr(data) {
|
||||
const redirected = data.qrUrl && /login\.taobao\.com|havanaone/.test(data.qrUrl)
|
||||
? '(已从闲鱼入口跳转到淘宝/阿里统一登录,这是官方登录链路)'
|
||||
: '';
|
||||
sub.textContent = `来源:${source}${redirected}。二维码过期就重新获取。`;
|
||||
const sizeHint = data.qrImageWidth && data.qrImageHeight ? ` 截图尺寸:${data.qrImageWidth}×${data.qrImageHeight}。` : '';
|
||||
sub.textContent = `来源:${source}${redirected}。二维码过期就重新获取。${sizeHint}`;
|
||||
}
|
||||
if (data.qrImage) {
|
||||
img.src = data.qrImage;
|
||||
|
||||
Reference in New Issue
Block a user