// Read visible UI signals only. Never inspect cookies, storage, form values or // private framework state. This is a login hint, not account identity proof. export const LOGIN_EVIDENCE = Object.freeze([ 'visible_current_account_navigation', 'visible_login_dialog', 'visible_login_entry', 'visible_verification_widget', 'no_conclusive_visible_evidence', 'unsupported_platform', 'inspection_unavailable', ]); export async function inspectVisibleSessionSignals(page) { return page.evaluate(() => { const visible = element => { const rect = element.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0 || rect.bottom <= 0 || rect.right <= 0 || rect.top >= innerHeight || rect.left >= innerWidth) return false; let top = Math.max(0, rect.top), bottom = Math.min(innerHeight, rect.bottom); let left = Math.max(0, rect.left), right = Math.min(innerWidth, rect.right); for (let node = element; node; node = node.parentElement) { const style = getComputedStyle(node); if (style.display === 'none' || ['hidden', 'collapse'].includes(style.visibility) || Number(style.opacity) === 0 || node.hasAttribute('hidden') || node.getAttribute('aria-hidden') === 'true') return false; if (node !== element) { const box = node.getBoundingClientRect(); if (/(?:auto|scroll|hidden|clip)/.test(style.overflowY)) { top = Math.max(top, box.top); bottom = Math.min(bottom, box.bottom); } if (/(?:auto|scroll|hidden|clip)/.test(style.overflowX)) { left = Math.max(left, box.left); right = Math.min(right, box.right); } } } return bottom > top && right > left; }; const label = element => { const walk = node => { if (node.nodeType === Node.TEXT_NODE) return node.textContent; if (node.nodeType !== Node.ELEMENT_NODE || node.matches('input,textarea,select,script,style,noscript,iframe,[contenteditable=true]') || !visible(node)) return ''; return [...node.childNodes].map(walk).join(' '); }; return [walk(element), element.getAttribute('aria-label'), element.getAttribute('title')].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim().slice(0, 1800); }; const pathname = element => { try { return new URL(element.getAttribute('href'), location.href).pathname; } catch { return ''; } }; const overlays = [...document.querySelectorAll('[role=dialog],[aria-modal=true],dialog[open],[class*="login-modal" i],[class*="login-container" i],[class*="login-dialog" i]')].filter(visible); const widgets = [...document.querySelectorAll('[class*="captcha" i],[id*="captcha" i],[class*="verification" i],[id*="verification" i],iframe[title*="验证"],iframe[title*="captcha" i],input[autocomplete="one-time-code"]')].filter(visible); const verification = widgets.some(element => element.matches('input,iframe') || /安全验证|请完成验证|拖动|滑动|点击.*验证|验证码|captcha|verification/i.test(label(element))); const loginDialog = overlays.some(element => /扫码登录|扫描二维码|手机号登录|验证码登录|账号登录|登录后|登录小红书|log\s?in|sign\s?in/i.test(label(element))); const entries = [...document.querySelectorAll('button,a,[role=button],[class*="login-btn" i],[class*="login-button" i]')].filter(visible); const loginEntry = entries.some(element => /^(?:登录|立即登录|去登录|登录\s*[/·]\s*注册|登录小红书|log\s?in|sign\s?in)$/i.test(label(element))); const profiles = [...document.querySelectorAll('a[href*="/user/profile/"]')].filter(element => visible(element) && /^\/user\/profile\/[A-Za-z0-9_-]{6,}\/?$/.test(pathname(element))); const currentAccountNavigation = profiles.some(profile => { const container = profile.closest('nav,aside,[role=navigation],ul'); if (!container || !visible(container)) return false; const links = [...container.querySelectorAll('a[href]')].filter(visible); // A note author's public profile link is not proof that the viewer signed // in. Require the profile link to belong to the visible main navigation. return links.some(link => pathname(link) === '/explore') && links.some(link => /^\/(?:message|notification)(?:\/|$)/.test(pathname(link))); }); return { verification, loginDialog, loginEntry, currentAccountNavigation }; }); } export function classifyLoginSignals(signals) { if (signals.verification) return { state: 'verification_required', evidence: ['visible_verification_widget'] }; if (signals.loginDialog) return { state: 'logged_out', evidence: ['visible_login_dialog'] }; if (signals.loginEntry && signals.currentAccountNavigation) return { state: 'unknown', evidence: ['visible_login_entry', 'visible_current_account_navigation'] }; if (signals.loginEntry) return { state: 'logged_out', evidence: ['visible_login_entry'] }; if (signals.currentAccountNavigation) return { state: 'logged_in', evidence: ['visible_current_account_navigation'] }; return { state: 'unknown', evidence: ['no_conclusive_visible_evidence'] }; } export async function detectSession(page) { const result = value => ({ ...value, basis: 'visible_page', observed_at: new Date().toISOString() }); try { const host = new URL(page.url()).hostname; if (host !== 'xiaohongshu.com' && !host.endsWith('.xiaohongshu.com')) return result({ state: 'unknown', evidence: ['unsupported_platform'] }); return result(classifyLoginSignals(await inspectVisibleSessionSignals(page))); } catch { return result({ state: 'unknown', evidence: ['inspection_unavailable'] }); } }