/** * 会话 Cookie 持久化:按 sessionKey 保存 cf_clearance 等,降低重复过盾概率 */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { RUNTIME_DIR, SESSION_TTL_MS } = require('./constants'); const SESSIONS_DIR = path.join(RUNTIME_DIR, 'sessions'); /** * 确保 sessions 目录存在 */ function ensureSessionsDir() { fs.mkdirSync(SESSIONS_DIR, { recursive: true, mode: 0o777 }); } /** * sessionKey 转安全文件名 * @param {string} sessionKey * @returns {string} */ function keyToFilename(sessionKey) { const hash = crypto.createHash('md5').update(sessionKey).digest('hex'); return `${hash}.json`; } /** * @typedef {Object} StoredSession * @property {string} sessionKey * @property {number} savedAt * @property {number} expiresAt * @property {Array<{name:string,value:string,domain?:string,path?:string}>} cookies */ /** * 加载已保存的会话 cookies * @param {string} sessionKey * @returns {StoredSession|null} */ function loadSession(sessionKey) { if (!sessionKey) return null; ensureSessionsDir(); const filePath = path.join(SESSIONS_DIR, keyToFilename(sessionKey)); if (!fs.existsSync(filePath)) return null; try { const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')); if (!raw || !Array.isArray(raw.cookies)) return null; if (raw.expiresAt && Date.now() > raw.expiresAt) { fs.unlinkSync(filePath); return null; } return raw; } catch (_) { return null; } } /** * 保存会话 cookies(优先保留 CF 相关 cookie) * @param {string} sessionKey * @param {Array<{name:string,value:string,domain?:string,path?:string}>} cookies * @param {number} [ttlMs] */ function saveSession(sessionKey, cookies, ttlMs = SESSION_TTL_MS) { if (!sessionKey || !Array.isArray(cookies) || cookies.length === 0) return; ensureSessionsDir(); const cfRelated = cookies.filter((c) => ['cf_clearance', '__cf_bm', 'cf_chl_2'].includes(c.name) || c.name.startsWith('__cf') ); const toSave = cfRelated.length > 0 ? cfRelated : cookies; const now = Date.now(); const payload = { sessionKey, savedAt: now, expiresAt: now + ttlMs, cookies: toSave.map((c) => ({ name: c.name, value: c.value, domain: c.domain, path: c.path || '/', })), }; const filePath = path.join(SESSIONS_DIR, keyToFilename(sessionKey)); fs.writeFileSync(filePath, JSON.stringify(payload, null, 0), { mode: 0o666 }); } /** * 将会话 cookies 转为 puppeteer setCookie 格式 * @param {StoredSession|null} session * @param {string} pageUrl * @returns {Array<{name:string,value:string,domain?:string,path?:string,url?:string}>} */ function sessionCookiesForPage(session, pageUrl) { if (!session || !Array.isArray(session.cookies)) return []; let hostname = ''; try { hostname = new URL(pageUrl).hostname; } catch (_) { return session.cookies; } return session.cookies.map((c) => { const cookie = { ...c }; if (!cookie.domain && hostname) { cookie.url = `https://${hostname}/`; } return cookie; }); } module.exports = { loadSession, saveSession, sessionCookiesForPage, };