import { getBrowserContext } from './browser.mjs'; import { sleep } from './utils.mjs'; const HOME_URL = 'https://www.goofish.com/'; const SEARCH_PROBE_URL = 'https://www.goofish.com/search?q=e3%201245%20v3'; const QR_LOGIN_URL = 'https://www.goofish.com/'; function accountTextValue(bodyText, labels) { const lines = String(bodyText || '').split(/\n+/).map(s => s.trim()).filter(Boolean); for (const label of labels) { const idx = lines.findIndex(line => line === label || line.startsWith(`${label}:`) || line.startsWith(`${label}:`)); if (idx >= 0) { const inline = lines[idx].replace(new RegExp(`^${label}[::]?\\s*`), '').trim(); if (inline && inline !== label) return inline; if (lines[idx + 1]) return lines[idx + 1]; } } return ''; } async function extractAccountInfo(page, bodyText) { const title = await page.title().catch(() => ''); const nickTexts = await page.locator('[class*="nick--"], [class*="user-name"], [class*="nickname"], [class*="userName"]').evaluateAll(nodes => ( nodes.map(n => n.textContent?.trim()).filter(Boolean).slice(0, 5) )).catch(() => []); const avatarUrl = await page.locator('[class*="user-order-container"] img, img[class*="avatar"], [class*="avatar"] img').first().getAttribute('src', { timeout: 1200 }).catch(() => ''); const accountName = nickTexts[0] || accountTextValue(bodyText, ['昵称', '用户名', '账号']) || ''; return { accountName, nickCandidates: nickTexts, avatarUrl: avatarUrl || '', pageTitle: title, }; } function normalizeCookieDomain(domain = '') { const d = String(domain || '').trim(); if (!d) return '.goofish.com'; if (/^(goofish\.com|www\.goofish\.com|2\.taobao\.com|taobao\.com|login\.taobao\.com|alibaba\.com)$/i.test(d)) { return `.${d.replace(/^\./, '')}`; } return d; } function normalizeCookie(cookie) { if (!cookie || typeof cookie !== 'object') return null; const name = String(cookie.name || '').trim(); const value = cookie.value == null ? '' : String(cookie.value); if (!name) return null; const normalized = { name, value, domain: normalizeCookieDomain(cookie.domain), path: cookie.path || '/', httpOnly: !!cookie.httpOnly, secure: cookie.secure !== false, sameSite: ['Strict', 'Lax', 'None'].includes(cookie.sameSite) ? cookie.sameSite : 'Lax', }; const rawExpires = cookie.expires ?? cookie.expirationDate ?? cookie.expiry; if (rawExpires != null && rawExpires !== -1 && rawExpires !== 'Session') { const expires = Number(rawExpires); if (Number.isFinite(expires) && expires > 0) { normalized.expires = expires > 1e12 ? Math.floor(expires / 1000) : Math.floor(expires); } } return normalized; } export function parseCookiesPayload(payload) { let data = payload; if (typeof data === 'string') data = JSON.parse(data); const cookies = Array.isArray(data) ? data : Array.isArray(data?.cookies) ? data.cookies : Array.isArray(data?.data) ? data.data : null; if (!cookies) throw new Error('Cookie JSON 格式不正确:请上传 Cookie 数组,或包含 cookies 数组的 JSON 对象'); const normalized = cookies.map(normalizeCookie).filter(Boolean); if (!normalized.length) throw new Error('没有识别到有效 Cookie;每条 Cookie 至少需要 name/value/domain'); return normalized; } export async function importCookiesFromJson(payload, emitLog = () => {}) { const cookies = parseCookiesPayload(payload); const ctx = await getBrowserContext(); await ctx.addCookies(cookies); emitLog(`✅ 已导入 ${cookies.length} 条 Cookie 到 Playwright 持久化浏览器`); const storedCookies = await ctx.cookies().catch(() => []); const goofishCookies = storedCookies.filter(c => /(^|\.)goofish\.com$|(^|\.)taobao\.com$|(^|\.)alibaba\.com$/.test(c.domain)); return { imported: cookies.length, status: { loggedIn: goofishCookies.length > 5, loginWall: false, loginTextVisible: false, nickVisible: false, avatarVisible: false, cardCount: 0, cookieCount: goofishCookies.length, accountName: '', nickCandidates: [], avatarUrl: '', url: 'cookie-import://local', title: 'Cookie 已导入,点击“检测登录状态”可联网验证', checkedAt: new Date().toISOString(), storage: 'browser-data/', }, }; } export async function openLoginPage(emitLog = () => {}) { const ctx = await getBrowserContext(); const page = ctx.pages()[0] || await ctx.newPage(); emitLog('正在打开闲鱼登录/首页窗口...'); await page.goto(HOME_URL, { waitUntil: 'domcontentloaded', timeout: 30000 }); await sleep(2000); const status = await getLoginStatus(page); if (status.loggedIn) emitLog('✅ 已检测到登录状态,cookie 已保存在 browser-data 中'); else emitLog('⚠️ 还未登录:请在弹出的 Chromium 窗口中完成扫码/验证码登录,完成后点击 Web 上的“检测登录状态”'); return status; } export async function getLoginQr(emitLog = () => {}) { const ctx = await getBrowserContext(); const page = ctx.pages().find(p => /goofish\.com|login\.taobao\.com|havanaone/.test(p.url())) || ctx.pages()[0] || await ctx.newPage(); emitLog('正在打开闲鱼 Web 登录入口并生成后台二维码截图...'); await page.goto(QR_LOGIN_URL, { waitUntil: 'domcontentloaded', timeout: 30000 }); await sleep(2500); let clicked = false; for (const selector of ['text=立即登录', 'text=登录', 'button:has-text("登录")', 'a:has-text("登录")']) { const loc = page.locator(selector).first(); if (await loc.isVisible({ timeout: 1000 }).catch(() => false)) { await loc.click({ timeout: 3000 }).catch(() => {}); clicked = true; await sleep(2500); break; } } for (const selector of ['text=扫码登录', 'text=二维码登录', 'text=使用二维码登录', '.icon-qrcode', '[class*=qrcode]']) { const loc = page.locator(selector).first(); if (await loc.isVisible({ timeout: 1000 }).catch(() => false)) { await loc.click({ timeout: 2500 }).catch(() => {}); await sleep(1500); break; } } const status = await getLoginStatus(page); let qrImage = ''; if (!status.loggedIn) { const modal = page.locator('[class*="login-modal"], [class*="Login"], [class*="qrcode"], [class*="qr"], iframe').first(); const target = await modal.isVisible({ timeout: 1500 }).catch(() => false) ? modal : page.locator('body'); const buf = await target.screenshot({ type: 'png', timeout: 8000 }).catch(() => null); if (buf) qrImage = `data:image/png;base64,${buf.toString('base64')}`; } if (status.loggedIn) emitLog('✅ 当前账号已登录,无需扫码'); else emitLog(`已生成登录截图${clicked ? '' : '(未找到明确登录按钮,已截取当前页面)'};请在后台页面扫码后点“检测登录状态”`); return { ...status, qrImage, qrCheckedAt: new Date().toISOString() }; } export async function getLoginStatus(existingPage = null) { const ctx = await getBrowserContext(); const page = existingPage || ctx.pages()[0] || await ctx.newPage(); if (!existingPage) { await page.goto(SEARCH_PROBE_URL, { waitUntil: 'domcontentloaded', timeout: 30000 }).catch(async () => { await page.goto(HOME_URL, { waitUntil: 'domcontentloaded', timeout: 30000 }); }); await sleep(3000); } const bodyText = await page.locator('body').innerText({ timeout: 3000 }).catch(() => ''); const hasLoginWall = /登录后可以更懂你|立即登录|请登录/.test(bodyText); const loginTextVisible = await page.locator('text=登录').first().isVisible({ timeout: 1000 }).catch(() => false); const nickVisible = await page.locator('[class*="nick--"]').first().isVisible({ timeout: 1500 }).catch(() => false); const avatarVisible = await page.locator('[class*="user-order-container"] img').first().isVisible({ timeout: 1500 }).catch(() => false); const cardCount = await page.locator('a[class*="feeds-item-wrap"], a[href*="/item"], a[href*="id="]').count().catch(() => 0); const cookies = await ctx.cookies().catch(() => []); const goofishCookies = cookies.filter(c => /(^|\.)goofish\.com$|(^|\.)taobao\.com$|(^|\.)alibaba\.com$/.test(c.domain)); const likelyLoggedIn = !hasLoginWall && !loginTextVisible && (nickVisible || avatarVisible || cardCount > 0 || goofishCookies.length > 5); const accountInfo = likelyLoggedIn ? await extractAccountInfo(page, bodyText) : { accountName: '', nickCandidates: [], avatarUrl: '', pageTitle: await page.title().catch(() => '') }; return { loggedIn: likelyLoggedIn, loginWall: hasLoginWall, loginTextVisible, nickVisible, avatarVisible, cardCount, cookieCount: goofishCookies.length, accountName: accountInfo.accountName, nickCandidates: accountInfo.nickCandidates, avatarUrl: accountInfo.avatarUrl, url: page.url(), title: accountInfo.pageTitle, checkedAt: new Date().toISOString(), storage: 'browser-data/', }; } export async function checkLogin(emitLog) { const ctx = await getBrowserContext(); const page = ctx.pages()[0] || await ctx.newPage(); emitLog('正在打开闲鱼首页...'); await page.goto(HOME_URL, { waitUntil: 'domcontentloaded', timeout: 30000 }); await sleep(3000); for (let attempt = 0; attempt < 5; attempt++) { const status = await getLoginStatus(page); if (status.loggedIn) { emitLog(`✅ 登录状态正常,cookie 数 ${status.cookieCount},持久化目录 browser-data/`); return true; } const remaining = 5 - attempt - 1; emitLog(`⚠️ 未检测到登录状态,请在浏览器中手动登录(剩余${remaining}次重试机会)`); emitLog('等待20秒后重新检测...'); await sleep(20000); await page.reload({ waitUntil: 'domcontentloaded', timeout: 20000 }).catch(() => {}); await sleep(3000); } emitLog('❌ 多次检测均未登录,请登录后重新启动任务'); return false; }