import { EventEmitter } from "events"; import { connect } from "puppeteer-real-browser"; import { v4 as uuidV4 } from "uuid"; import path from "path"; import { fileURLToPath } from "url"; import { createDirectoryIfNotExists, sleep, extractPerplexityCookie, getPerplexitySessionCookie } from "../utils/cookieUtils.mjs"; import '../proxyAgent.mjs'; import { detectBrowser } from '../utils/browserDetector.mjs'; import NetworkMonitor from '../networkMonitor.mjs'; import io from 'socket.io-client'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); class PerplexityProvider { constructor(config) { this.config = config; this.sessions = {}; // 可以是 'chrome', 'edge', 或 'auto' this.preferredBrowser = 'auto'; this.networkMonitor = new NetworkMonitor(); } async init(config) { console.log(`本项目依赖Chrome或Edge浏览器,请勿关闭弹出的浏览器窗口。如果出现错误请检查是否已安装Chrome或Edge浏览器。`); const browserPath = detectBrowser(this.preferredBrowser); // 检测Chrome和Edge浏览器 this.sessions = {}; const timeout = 120000; if (process.env.USE_MANUAL_LOGIN === "true") { this.sessions['manual_login'] = { configIndex: 0, valid: false, }; console.log("当前使用手动登录模式,跳过config.mjs文件中的 cookie 验证"); } else { // 使用配置文件中的 cookie for (let index = 0; index < config.sessions.length; index++) { const session = config.sessions[index]; const extractedCookie = extractPerplexityCookie(session.cookie); if (extractedCookie.sessionToken) { const uniqueId = `user_${index}`; this.sessions[uniqueId] = { configIndex: index, ...extractedCookie, valid: false, }; console.log(`已添加 #${index} ${uniqueId} (Perplexity cookie)`); } else { console.error(`第${index}个cookie无效,请重新获取。`); console.error(`未检测到有效的__Secure-next-auth.session-token字段。`); } } console.log(`已添加 ${Object.keys(this.sessions).length} 个 cookie,开始验证有效性`); } for (const originalUsername of Object.keys(this.sessions)) { let currentUsername = originalUsername; let session = this.sessions[currentUsername]; createDirectoryIfNotExists(path.join(__dirname, "browser_profiles", currentUsername)); try { const response = await connect({ headless: "auto", turnstile: true, customConfig: { userDataDir: path.join(__dirname, "browser_profiles", currentUsername), executablePath: browserPath, }, }); const {page, browser} = response; if (process.env.USE_MANUAL_LOGIN === "true") { console.log(`正在为 session #${session.configIndex} 进行手动登录...`); await page.goto("https://www.perplexity.ai", {timeout: timeout}); await sleep(3000); console.log(`请在打开的浏览器窗口中手动登录 Perplexity.ai (session #${session.configIndex})`); const { sessionCookie, accountStatus } = await this.waitForManualLogin(page); if (sessionCookie) { const email = accountStatus.username || 'unknown_user'; this.sessions[email] = { ...session, cookies: sessionCookie, isPro: accountStatus.isPro, }; delete this.sessions[currentUsername]; currentUsername = email; session = this.sessions[currentUsername]; console.log(`成功获取 ${email} 登录的 cookie${accountStatus.isPro ? '(Pro 账号)' : ''}`); } else { console.error(`未能获取到 session #${session.configIndex} 有效登录的 cookie`); await browser.close(); continue; } } else { // 使用已有的 cookie const perplexityCookies = getPerplexitySessionCookie(session); await page.setCookie(...perplexityCookies); await page.goto("https://www.perplexity.ai", {timeout: timeout}); await sleep(5000); } // 如果遇到 Cloudflare 挑战就多等一段时间 const pageContent = await page.content(); if (pageContent.indexOf("challenges.cloudflare.com") > -1) { console.log(`请在30秒内完成人机验证 (${currentUsername})`); await page.evaluate(() => { alert("请在30秒内完成人机验证"); }); await sleep(30000); } try { const isValid = await this.validateSession(page); if (isValid) { session.valid = true; session.browser = browser; session.page = page; } else { console.warn(`警告: ${currentUsername} 验证失败。请检查cookie是否有效。`); await this.clearPerplexityCookies(page); await browser.close(); } } catch (e) { console.warn(`警告: ${currentUsername} 验证失败。请检查cookie是否有效。`); console.error(e); await this.clearPerplexityCookies(page); await browser.close(); } } catch (e) { console.error(`初始化浏览器失败 (${currentUsername})`); console.error(e); } } console.log(`验证完毕,有效cookie数量 ${Object.keys(this.sessions).filter((username) => this.sessions[username].valid).length}`); // 开始网络监控 await this.networkMonitor.startMonitoring(); } async clearPerplexityCookies(page) { const client = await page.target().createCDPSession(); await client.send('Network.clearBrowserCookies'); await client.send('Network.clearBrowserCache'); const cookies = await page.cookies('https://www.perplexity.ai'); for (const cookie of cookies) { await page.deleteCookie(cookie); } console.log('已自动清理 cookie'); } async checkAccountStatus(page) { if (process.env.USE_MANUAL_LOGIN !== "true" && process.env.INCOGNITO_MODE === "true") { try { await page.waitForSelector('div[class*="inline-flex w-full cursor-pointer select-none items-center justify-start gap-sm rounded-full"]', { timeout: 2000 }); // 检查无痕模式图标 const isIncognito = await page.evaluate(() => { const incognitoElement = document.querySelector('div[class*="inline-flex w-full cursor-pointer select-none items-center justify-start gap-sm rounded-full"]'); return incognitoElement && incognitoElement.innerHTML.includes('viewBox="0 0 24 24"'); }); if (isIncognito) { return { isPro: false, username: 'Incognito User', isIncognito: true }; } else { return { isPro: false, username: null, isIncognito: false }; } } catch (error) { return { isPro: false, username: null, isIncognito: false }; } } else { try { await page.waitForSelector('div[class*="relative flex items-center gap-x-xs"]', { timeout: 2000 }); const isPro = await page.evaluate(() => { const svgElement = document.querySelector('div[class*="relative flex aspect-square"] svg'); return svgElement && svgElement.innerHTML.includes('fill-super dark:fill-superDark'); }); const username = await page.evaluate(() => { const usernameElement = document.querySelector('div[class*="relative flex items-center gap-x-xs"] div'); return usernameElement ? usernameElement.textContent.trim() : null; }); return { isPro, username, isIncognito: false }; } catch (error) { return { isPro: false, username: null, isIncognito: false }; } } } async waitForManualLogin(page) { return new Promise(async (resolve) => { let isResolved = false; const checkLoginCompletion = async () => { if (isResolved) return; try { const accountStatus = await this.checkAccountStatus(page); if (accountStatus.username) { console.log(`登录成功: ${accountStatus.username}`); const cookies = await page.cookies(); const sessionCookie = this.extractPerplexitySessionCookie(cookies); isResolved = true; resolve({ sessionCookie, accountStatus }); } } catch (error) { // 如果检查失败,继续等待 } }; // 监听 DOM 变化 await page.evaluate(() => { const observer = new MutationObserver((mutations) => { for (let mutation of mutations) { if (mutation.type === 'childList' || mutation.type === 'subtree') { window.dispatchEvent(new CustomEvent('domChanged')); break; } } }); observer.observe(document.body, { childList: true, subtree: true, }); }); // 监听 DOM 变化 await page.exposeFunction('notifyDomChanged', checkLoginCompletion); await page.evaluate(() => { window.addEventListener('domChanged', window.notifyDomChanged); }); // 监听导航完成 page.on('load', checkLoginCompletion); setTimeout(() => { if (!isResolved) { console.log('登录等待超时'); resolve({ sessionCookie: null, accountStatus: null }); } }, 300000); // 初始检查 await checkLoginCompletion(); }); } async validateSession(page) { const accountStatus = await this.checkAccountStatus(page); if (accountStatus.username || accountStatus.isIncognito) { if (accountStatus.isIncognito) { console.log('无痕模式验证成功'); } else { console.log(`账号 ${accountStatus.username} 登录有效${accountStatus.isPro ? '(Pro 账号)' : ''}`); } return true; } else { console.log('账号验证失败'); return false; } } extractPerplexitySessionCookie(cookies) { let sessionCookie = []; const sessionToken = cookies.find(c => c.name === '__Secure-next-auth.session-token'); if (sessionToken) { sessionCookie.push(sessionToken); } if (sessionCookie.length === 0) { console.error('无法提取有效的会话 cookie'); return null; } return sessionCookie; } async getCompletion({ username, messages, stream = false, proxyModel }) { if (this.networkMonitor.isNetworkBlocked()) { throw new Error("网络异常,请稍后再试"); } const session = this.sessions[username]; if (!session || !session.valid) { throw new Error(`用户 ${username} 的会话无效`); } const { page } = session; const emitter = new EventEmitter(); // 转换纯文本 let previousMessages = messages.map(msg => msg.content).join("\n\n"); // 生成必要的参数 const messagePayload = { version: "2.13", source: "default", language: "en-GB", timezone: "Europe/London", search_focus: "writing", frontend_uuid: uuidV4(), mode: "concise", is_related_query: false, is_default_related_query: false, visitor_id: uuidV4(), user_nextauth_id: uuidV4(), frontend_context_uuid: uuidV4(), prompt_source: "user", query_source: "home", }; await page.addScriptTag({ url: 'https://cdn.socket.io/4.4.1/socket.io.min.js' }); const ioExists = await page.evaluate(() => typeof io !== 'undefined'); if (!ioExists) { throw new Error("socket.io 客户端脚本加载失败"); } // 移除之前 console page.removeAllListeners('console'); page.on('console', msg => { const text = msg.text(); console.log(text); }); // 唯一的回调函数名 const callbackName = `nodeCallback_${uuidV4()}`; // 在浏览器上下文中建立 WebSocket 连接并发送消息 await page.exposeFunction(callbackName, (event, data) => { if (event === "completion") { const { id, text } = data; emitter.emit("completion", id, text); } else if (event === "end") { emitter.emit("end"); } else if (event === "error") { console.error("Error from page.evaluate:", data); emitter.emit("error", data); } }); await page.evaluate( (previousMessages, messagePayload, callbackName, stream) => { try { if (typeof io === 'undefined') { window[callbackName]("error", "Socket.io 客户端未定义,请检查脚本是否正确加载。"); return; } if (window.socket) { // 如果已有 socket 连接 window.socket.disconnect(); window.socket = null; } // 累积接收到的 chunk(用于非流) let accumulatedChunks = ''; const socket = io("wss://www.perplexity.ai/", { path: "/socket.io", transports: ["websocket"], }); window.socket = socket; // 定义监听器函数 function onQueryProgress(data) { try { if (data.text) { const textData = JSON.parse(data.text); const chunk = textData.chunks[textData.chunks.length - 1]; if (chunk) { if (stream) { // 实时发送 chunk console.log(chunk); // 直接输出 chunk 内容 window[callbackName]("completion", { id: messagePayload.frontend_uuid, text: chunk }); } else { // 累积 chunk accumulatedChunks += chunk; } } } // 检查是否为最后一条消息 if (data.final) { if (!stream) { // 非流发送完整的响应 console.log(accumulatedChunks); window[callbackName]("completion", { id: messagePayload.frontend_uuid, text: accumulatedChunks }); } console.log("请求结束"); window[callbackName]("end"); // 清理资源 socket.off("query_progress", onQueryProgress); socket.disconnect(); window.socket = null; delete window[callbackName]; } } catch (err) { console.error("Error processing query_progress data:", err); window[callbackName]("error", err.toString()); } } // 添加监听器 socket.on("query_progress", onQueryProgress); socket.on("connect", () => { // 使用回调处理确认响应 socket.emit("perplexity_ask", previousMessages, messagePayload, (response) => { }); }); socket.on("error", (error) => { console.error("Socket error:", error); window[callbackName]("error", error.toString()); }); socket.on("connect_error", (error) => { console.error("Socket connect_error:", error); window[callbackName]("error", error.toString()); }); socket.on("connect_timeout", (timeout) => { console.error("Socket connect_timeout:", timeout); window[callbackName]("error", "Connection timed out"); }); } catch (err) { console.error("Error in page.evaluate:", err); window[callbackName]("error", err.toString()); } }, previousMessages, messagePayload, callbackName, stream ); const cancel = () => { page.evaluate((callbackName) => { if (window.socket) { window.socket.disconnect(); window.socket = null; } if (window[callbackName]) { delete window[callbackName]; } }, callbackName).catch(console.error); }; // 触发 'start' emitter.emit("start", messagePayload.frontend_uuid); return { completion: emitter, cancel }; } } export default PerplexityProvider;