Add files via upload
This commit is contained in:
@@ -1,467 +1,468 @@
|
||||
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.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 = {};
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user