refactor: restructure codebase for v1 kasm docker release

This commit is contained in:
Kovasky Buezo
2026-07-06 21:19:16 -02:30
parent 8176fce0c7
commit 1c2b963f27
34 changed files with 7589 additions and 8447 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules
browser_profiles
.git
.env
*.tar
+2
View File
@@ -7,3 +7,5 @@ test.mjs
config.js
config.mjs
browser_profiles
*.log
local_copy_formatted_messages.txt
+45 -27
View File
@@ -1,37 +1,55 @@
# Use the official Node.js 20 image as the base image
FROM node:20
RUN chmod 1777 /tmp
# Install necessary dependencies for running Chrome
RUN apt-get update && apt-get install -y \
wget \
gnupg \
ca-certificates \
apt-transport-https \
xvfb \
&& rm -rf /var/lib/apt/lists/*
FROM kasmweb/chrome:1.19.0
# Install Google Chrome
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& echo "deb [arch=amd64] https://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list \
&& apt-get update \
&& apt-get install -y google-chrome-stable \
&& rm -rf /var/lib/apt/lists/*
USER root
# Set up the working directory in the container
# Install Node.js 20.x
RUN rm -f /etc/apt/sources.list.d/google-chrome.list && \
apt-get update && \
apt-get install -y curl && \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Create app directory and set ownership to kasm-user
RUN mkdir -p /app && chown -R 1000:1000 /app
# Switch to kasm-user to run npm install safely
USER 1000
WORKDIR /app
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Copy application files (ignoring node_modules and browser_profiles)
COPY --chown=1000:1000 . /app
# Install Node.js dependencies
# Pre-create the browser_profiles and data directory so the volume inherits the correct ownership
RUN mkdir -p /app/browser_profiles /app/data && chown -R 1000:1000 /app/browser_profiles /app/data
# Install Node dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Create a custom entrypoint script
USER root
RUN echo '#!/bin/bash\n\
# Start Kasm VNC server in the background\n\
/dockerstartup/vnc_startup.sh &\n\
\n\
# Wait for the X server to fully initialize\n\
sleep 5\n\
\n\
# Set environment variables for YOUChat_Proxy\n\
export USE_MANUAL_LOGIN="true"\n\
export HIDE_BROWSER="false"\n\
export BROWSER_PATH="/usr/bin/google-chrome"\n\
export DISPLAY=:1\n\
\n\
# Start the proxy server\n\
cd /app\n\
node src/index.mjs\n\
' > /app/start-proxy.sh && chmod +x /app/start-proxy.sh
# Expose the port your app runs on
EXPOSE 8080
USER 1000
# Command to run the application
# Expose Kasm VNC port and Proxy API port
EXPOSE 6901 8080
CMD [ "node", "index.mjs" ]
ENTRYPOINT ["/app/start-proxy.sh"]
+6 -10
View File
@@ -1,5 +1,5 @@
# 更新日志(2025年4月4日)
添加对gemini 2.5 pro的支持(与官方api相比不支持联网搜索)
# Changelog (April 4, 2025)
Added support for gemini 2.5 pro (does not support internet search compared to the official API)
---------------------------------------------------------
@@ -7,22 +7,18 @@
A proxy for YOU Chat.
YOU.Com 转换为通用代理。
如果您觉得本项目对您有帮助,请考虑[请我喝杯蜜雪冰城](https://github.com/sponsors/Archeb?frequency=one-time)
Convert YOU.Com into a generic proxy.
If you find this project useful, please consider [buying me a cup of coffee](https://github.com/sponsors/Archeb?frequency=one-time);
[**Usage 使用方法**](usage.md)
[**Usage**](usage.md)
**It is forbidden to use this project for profit.**
**仅供个人部署用于访问自己合法取得的订阅,严禁用于转售或其他商业用途。不提供任何技术支持、不为任何违规使用导致的封号负责。**
**This is for personal deployment only to access subscriptions you legally obtain. It is strictly prohibited for resale or other commercial purposes. No technical support is provided, and we are not responsible for any account bans caused by misuse.**
ASK THE ASSISTANT TO HELP YOU DEPLOY ↓
有部署问题请问部署助手: https://www.coze.com/store/bot/7383564439096131592?bot_id=true
您的电脑上必须安装有 Google Chrome 或者 Edge 浏览器才能够使用当前分支。旧版已不再维护。
If you have deployment issues, ask the deployment assistant: https://www.coze.com/store/bot/7383564439096131592?bot_id=true
Google Chrome must be installed on your system in order to use this proxy. Previous versions are no longer maintained.
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+22
View File
@@ -0,0 +1,22 @@
version: "3.8"
services:
chat-proxy:
build: .
container_name: youchat-proxy
ports:
- "8080:8080"
environment:
- NODE_ENV=production
- PORT=8080
- PASSWORD=${PASSWORD:-}
restart: unless-stopped
volumes:
- ./data:/app/data
- ./browser_profiles:/app/browser_profiles
- ./config.mjs:/app/config.mjs
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
-262
View File
@@ -1,262 +0,0 @@
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} from "../utils/cookieUtils.mjs";
import '../proxyAgent.mjs';
import NetworkMonitor from "../networkMonitor.mjs";
import {detectBrowser} from "../utils/browserDetector.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class HappyApiProvider {
constructor(config) {
this.config = config;
this.sessions = {};
this.preferredBrowser = 'auto';
this.networkMonitor = new NetworkMonitor();
}
async init() {
console.log(`本项目依赖 Chrome 或 Edge 浏览器,请勿关闭弹出的浏览器窗口。如果出现错误请检查是否已安装 Chrome 或 Edge 浏览器。`);
const browserPath = detectBrowser(this.preferredBrowser);
this.sessions = {};
// 手动登录
const currentUsername = 'manual_login';
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;
console.log(`请在打开的浏览器窗口中手动登录 happyapi.org`);
await page.goto("https://happyapi.org", {waitUntil: 'networkidle0'});
await sleep(3000); // 等待页面加载完毕
const sessionCookie = await this.waitForManualLogin(page);
if (sessionCookie) {
this.sessions[currentUsername] = {browser, page, sessionCookie, valid: true};
console.log(`成功获取会话 cookie`);
} else {
console.error(`未能检测到登录状态,请确保已成功登录`);
await browser.close();
}
} catch (error) {
console.error(`初始化浏览器失败`);
console.error(error);
}
// 开始网络监控
await this.networkMonitor.startMonitoring();
}
async waitForManualLogin(page) {
return new Promise((resolve) => {
const checkLoginStatus = async () => {
// 检查是否存在特定的登录成功标识元素
const isLoggedIn = await this.checkLoginStatus(page);
if (isLoggedIn) {
console.log(`检测到登录成功`);
const cookies = await page.cookies();
const sessionCookie = this.extractSessionCookie(cookies);
resolve(sessionCookie);
} else {
setTimeout(checkLoginStatus, 1000);
}
};
page.on('request', (request) => {
if (request.url() === 'https://happyapi.org/api/v1/auths/signin' && request.method() === 'POST') {
console.log('检测到登录请求');
checkLoginStatus();
}
});
checkLoginStatus();
});
}
extractSessionCookie(cookies) {
const cfClearance = cookies.find(c => c.name === 'cf_clearance')?.value;
const token = cookies.find(c => c.name === 'token')?.value;
if (cfClearance && token) {
return [
{
name: 'cf_clearance',
value: cfClearance,
domain: 'happyapi.org',
path: '/',
httpOnly: true,
secure: true,
sameSite: 'Lax',
},
{
name: 'token',
value: token,
domain: 'happyapi.org',
path: '/',
httpOnly: true,
secure: true,
sameSite: 'Lax',
}
];
} else {
console.error('无法提取有效的会话 cookie');
return null;
}
}
async checkLoginStatus(page) {
try {
const userNameElement = await page.waitForSelector('div.self-center.font-medium', {timeout: 5000});
const userName = await page.evaluate(element => element.textContent, userNameElement);
return true;
} catch {
return false;
}
}
async getCompletion({username, messages, stream = false, proxyModel}) {
const session = this.sessions[username];
if (!session || !session.valid) {
throw new Error(`用户 ${username} 的会话无效`);
}
//刷新页面
await session.page.goto("https://happyapi.org", {waitUntil: 'domcontentloaded'});
const {page} = session;
const emitter = new EventEmitter();
try {
let userQuery = messages.map(msg => msg.content).join("\n\n");
const inputSelector = 'textarea#chat-textarea';
const sendButtonSelector = 'button#send-message-button';
// 输入消息
await page.waitForSelector(inputSelector, {visible: true});
await page.evaluate((selector, message) => {
const textarea = document.querySelector(selector);
textarea.value = message;
// 手动触发事件
textarea.dispatchEvent(new Event('input', {bubbles: true}));
textarea.dispatchEvent(new Event('change', {bubbles: true}));
}, inputSelector, userQuery.trim());
await page.waitForTimeout(100);
// 点击发送按钮
await page.waitForSelector(sendButtonSelector, {visible: true});
await page.click(sendButtonSelector);
// 监听请求
const targetUrl = 'https://happyapi.org/api/chat/completions';
const traceId = uuidV4();
let finalResponse = ""; // 存储最终响应
let responseStarted = false; // 是否开始接收
let isEnding = false; // 是否正在结束
// 清理函数
const cleanup = async () => {
clearTimeout(responseTimeout);
page.removeListener('response', responseHandler);
};
const responseTimeout = setTimeout(() => {
if (!isEnding) {
isEnding = true;
emitter.emit('error', new Error('请求超时'));
cleanup();
}
}, 60000); // 60秒,可根据需要调整
const responseHandler = async (response) => {
if (response.url() === targetUrl) {
try {
const buffer = await response.buffer();
const text = buffer.toString('utf8');
// 处理流式数据
const lines = text.split('\n\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6).trim();
if (data === '[DONE]') {
if (!isEnding) {
isEnding = true;
console.log("请求结束");
emitter.emit('end');
cleanup();
}
return;
}
if (data.length === 0) {
continue;
}
const parsedData = JSON.parse(data);
const contentDelta = parsedData.choices[0].delta.content || '';
if (stream) {
emitter.emit('completion', traceId, contentDelta);
} else {
finalResponse += contentDelta;
}
}
}
} catch (e) {
if (!isEnding) {
isEnding = true;
console.error("请求发生错误", e);
emitter.emit('error', e);
cleanup();
}
}
}
};
// 监听response
page.on('response', responseHandler);
// 清除计时器和监听器
emitter.on('end', () => {
cleanup();
});
emitter.on('error', () => {
cleanup();
});
return {
completion: emitter,
cancel: () => {
}
};
} catch (error) {
emitter.emit('error', error);
return {
completion: emitter,
cancel: () => { }
};
}
}
}
export default HappyApiProvider;
-66
View File
@@ -1,66 +0,0 @@
const imageStorage = new Map();
let imageCounter = 0;
/**
* 保存图片到内存中
* @param {string} base64Data base64 数据
* @param {string} mediaType 媒体类型 ("image/jpeg")
* @returns {{ imageId: string, mediaType: string }} 返回图片信息
*/
export function storeImage(base64Data, mediaType) {
const randomPart = Math.floor(Math.random() * 10000);
const imageId = `image_${Date.now()}_${imageCounter++}_${randomPart}`;
imageStorage.set(imageId, { imageId, base64Data, mediaType });
// console.log(`Image stored with ID: ${imageId}, Media Type: ${mediaType}`);
// 打印存储的 base64
// console.log(`Base64 Data for Image ID ${imageId}: ${base64Data.substring(0, 100)}...`);
return { imageId, mediaType };
}
/**
* 获取图片数据
* @param {string} imageId 图片 ID
* @returns {object|null} base64 数据和媒体类型
*/
export function getImage(imageId) {
return imageStorage.get(imageId) || null;
}
/**
* 获取最后一个图片
* @returns {object|null} base64 数据和媒体类型
*/
export function getLastImage() {
const lastKey = Array.from(imageStorage.keys()).pop();
return lastKey ? imageStorage.get(lastKey) : null;
}
/**
* 删除图片数据
* @param {string} imageId 图片 ID
*/
export function removeImage(imageId) {
imageStorage.delete(imageId);
}
/**
* 清空所有存储的图片
*/
export function clearAllImages() {
imageStorage.clear();
imageCounter = 0;
// console.log("All images have been cleared from memory.");
}
/**
* 打印所有存储的图片
*/
export function printAllImages() {
console.log('Current images stored in memory:');
imageStorage.forEach((value, key) => {
console.log(`Image ID: ${key}, Media Type: ${value.mediaType}`);
console.log(`Base64 Data: ${value.base64Data.substring(0, 100)}...`);
});
}
+4 -14
View File
@@ -2,8 +2,9 @@
"name": "youchat-proxy",
"version": "1.0.0",
"description": "",
"main": "index.js",
"main": "src/index.mjs",
"scripts": {
"start": "node src/index.mjs",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
@@ -11,27 +12,16 @@
"dependencies": {
"async-mutex": "^0.5.0",
"cookie": "^0.6.0",
"cookie-parser": "^1.4.7",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"docx": "^8.5.0",
"express": "^4.19.2",
"form-data": "^4.0.0",
"geoip-lite": "^1.4.10",
"https-proxy-agent": "^7.0.5",
"localtunnel": "^2.0.2",
"ngrok": "^5.0.0-beta.2",
"node-fetch": "^3.3.2",
"puppeteer-core": "^24.4.0",
"puppeteer-real-browser": "^1.4.2",
"socket.io-client": "^4.8.0",
"socks-proxy-agent": "^8.0.4",
"unzipper": "^0.12.3",
"uuid": "^9.0.1",
"winston": "^3.17.0",
"ws": "^8.18.1"
},
"devDependencies": {
"playwright": "^1.51.0"
"uuid": "^9.0.1"
}
}
-8
View File
@@ -1,8 +0,0 @@
export const config = {
sessions: [
{
cookie: `...`
}
// 可以添加更多cookie
]
};
-468
View File
@@ -1,468 +0,0 @@
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;
-51
View File
@@ -1,51 +0,0 @@
import YouProvider from './you_providers/youProvider.mjs';
import PerplexityProvider from './perplexity_providers/perplexityProvider.mjs';
import HappyApiProvider from './happyapi_providers/happyApi.mjs';
import {config as youConfig} from './config.mjs';
import {config as perplexityConfig} from './perplexityConfig.mjs';
class ProviderManager {
constructor() {
// 根据环境变量初始化提供者
const activeProvider = process.env.ACTIVE_PROVIDER || 'you';
switch (activeProvider) {
case 'you':
this.provider = new YouProvider(youConfig);
break;
case 'perplexity':
this.provider = new PerplexityProvider(perplexityConfig);
break;
case 'happyapi':
this.provider = new HappyApiProvider();
break;
default:
throw new Error('Invalid ACTIVE_PROVIDER. Use "you", "perplexity", or "happyapi".');
}
console.log(`Initialized with ${activeProvider} provider.`);
}
async init() {
await this.provider.init(this.provider.config);
console.log(`Provider initialized.`);
}
async getCompletion(params) {
return this.provider.getCompletion(params);
}
getCurrentProvider() {
return this.provider.constructor.name;
}
getLogger() {
return this.provider.logger;
}
getSessionManager() {
return this.provider.sessionManager;
}
}
export default ProviderManager;
+74 -73
View File
@@ -1,24 +1,25 @@
import sysLogger from './utils/sysLogger.mjs';
export function formatMessages(messages, proxyModel, randomFileName) {
// 检查是否是 Claude 模型
// Check if it is a Claude model
const isClaudeModel = proxyModel.toLowerCase().includes('claude');
// 启用特殊前缀
// Enable special prefix
const USE_BACKSPACE_PREFIX = process.env.USE_BACKSPACE_PREFIX === 'true';
// 定义角色映射
// Define role mapping
const roleFeatures = getRoleFeatures(messages, isClaudeModel, USE_BACKSPACE_PREFIX);
// 清除第一条消息的 role
// Clear role of first message
messages = clearFirstMessageRole(messages);
messages = removeCustomRoleDefinitions(messages);
messages = convertRoles(messages, roleFeatures);
// 替换 content 中的角色
// Replace role in content
messages = replaceRolesInContent(messages, roleFeatures);
// 如果启用 clewd
// If clewd is enabled
const CLEWD_ENABLED = process.env.CLEWD_ENABLED === 'true';
if (CLEWD_ENABLED) {
messages = xmlPlotAllMessages(messages, roleFeatures);
@@ -27,7 +28,7 @@ export function formatMessages(messages, proxyModel, randomFileName) {
messages = messages.map((message) => {
let newMessage = { ...message };
if (typeof newMessage.content === 'string') {
// 1) 移除 </FORMAT LINE BREAK/> 标识
// 1) Remove </FORMAT LINE BREAK/> tag
let tempContent = newMessage.content.replace(/<\/FORMAT\s+LINE\s+BREAK\/>/g, '');
tempContent = tempContent.replace(/\n{3,}/g, '\n\n');
newMessage.content = tempContent;
@@ -39,9 +40,9 @@ export function formatMessages(messages, proxyModel, randomFileName) {
}
/**
* 将首条消息role置空
* @param {Array} messages - 消息数组
* @returns {Array} 处理后的消息数组
* roleSet to empty
* @param {Array} messages -
* @returns {Array}
*/
function clearFirstMessageRole(messages) {
if (!Array.isArray(messages) || messages.length === 0) {
@@ -57,14 +58,14 @@ function clearFirstMessageRole(messages) {
return processedMessages;
}
// 获取角色特征
// Get role characteristics
function getRoleFeatures(messages, isClaudeModel, useBackspacePrefix) {
let prefix = useBackspacePrefix ? '\u0008' : '';
let systemRole = `${prefix}${isClaudeModel ? 'System' : 'system'}`;
let userRole = `${prefix}${isClaudeModel ? 'Human' : 'user'}`;
let assistantRole = `${prefix}${isClaudeModel ? 'Assistant' : 'assistant'}`;
// 匹配自定义角色
// Match custom role
const rolePattern = /\[\|(\w+)::(.*?)\|\]/g;
let customRoles = {};
messages.forEach(message => {
@@ -100,7 +101,7 @@ function getRoleFeatures(messages, isClaudeModel, useBackspacePrefix) {
};
}
// 移除 messages 自定义角色格式
// Remove custom role format from messages
function removeCustomRoleDefinitions(messages) {
const rolePattern = /\[\|\w+::.*?\|]/g;
@@ -113,7 +114,7 @@ function removeCustomRoleDefinitions(messages) {
});
}
// 转换角色
// Convert role
function convertRoles(messages, roleFeatures) {
const {systemRole, userRole, assistantRole} = roleFeatures;
const roleMap = {
@@ -127,7 +128,7 @@ function convertRoles(messages, roleFeatures) {
let currentRole = message.role;
if (currentRole.startsWith('\u0008')) {
// 包含前缀不需要转换
// Contains prefix, no conversion needed
return message;
} else {
const roleKey = currentRole.toLowerCase();
@@ -137,9 +138,9 @@ function convertRoles(messages, roleFeatures) {
});
}
// 替换 content 中的角色定义
// Replace role definitions in content
function replaceRolesInContent(messages, roleFeatures) {
// 避免重复添加
// Avoid duplicate addition
const roleMap = {
'System:': roleFeatures.systemRole.replace(roleFeatures.prefix, '') + ':',
'system:': roleFeatures.systemRole.replace(roleFeatures.prefix, '') + ':',
@@ -150,7 +151,7 @@ function replaceRolesInContent(messages, roleFeatures) {
'assistant:': roleFeatures.assistantRole.replace(roleFeatures.prefix, '') + ':',
};
// 构建角色正则
// Build role regex
const escapedLabels = Object.keys(roleMap).map(label => escapeRegExp(label));
const prefixPattern = roleFeatures.prefix ? escapeRegExp(roleFeatures.prefix) : '';
@@ -160,14 +161,14 @@ function replaceRolesInContent(messages, roleFeatures) {
let newContent = message.content;
if (typeof newContent === 'string') {
// 仅替换段落开头角色
// Only replace role at beginning of paragraph
newContent = newContent.replace(roleNamesPattern, (match, p1, p2, p3) => {
const newRoleLabel = roleMap[p3] || p3;
const prefixToUse = p2 !== undefined ? p2 : roleFeatures.prefix;
return p1 + (prefixToUse || '') + newRoleLabel;
});
} else {
console.warn('message.content is not a string:', newContent);
sysLogger.warn('message.content is not a string:', newContent);
newContent = '';
}
@@ -179,24 +180,24 @@ function replaceRolesInContent(messages, roleFeatures) {
}
/**
* CLEWD 阶段对一组 messages 进行二次处理
* 若消息 content 不含 <|KEEP_ROLE|>则将 role 置空
* 然后调用 xmlHyperProcess 做多轮正则合并<@N>插入收尾清理
* CLEWD messages
* content <|KEEP_ROLE|> role Set to empty
* xmlHyperProcess <@N>Final cleanup
*
* @param {Array} messages - [{ role: 'user', content: '...' }, ...]
* @param {object} roleFeatures - { systemRole, userRole, assistantRole, prefix }
* @return {Array} 新的 messages
* @return {Array} messages
*/
export function xmlPlotAllMessages(messages, roleFeatures) {
return messages.map((msg) => {
if (!msg.content.includes('<|KEEP_ROLE|>')) {
msg = {
...msg,
role: '' // 置空
role: '' // Set to empty
};
}
// 核心调用
// Core call
const processed = xmlHyperProcess(msg.content, roleFeatures);
return { ...msg, content: processed };
@@ -204,23 +205,23 @@ export function xmlPlotAllMessages(messages, roleFeatures) {
}
/**
* 多轮 <regex order=1 / 2 / 3>
* MergeDisable 判断 + System => user 替换
* 段落合并 + <@N>插入
* 最终收尾 cleanup
* <regex order=1 / 2 / 3>
* MergeDisable + System => user
* + <@N>
* cleanup
*
* @param {string} originalContent
* @param {object} roleFeatures
* @returns {string} 处理后的文本
* @returns {string}
*/
function xmlHyperProcess(originalContent, roleFeatures) {
let content = originalContent;
let regexLogs = '';
// 第1轮正则 (order=1)
// 1st round regex (order=1)
[content, regexLogs] = xmlHyperRegex(content, 1, regexLogs);
// 检测 MergeDisable 标记
// Detect MergeDisable tag
const mergeDisable = {
all: content.includes('<|Merge Disable|>'),
system: content.includes('<|Merge System Disable|>'),
@@ -228,27 +229,27 @@ function xmlHyperProcess(originalContent, roleFeatures) {
assistant: content.includes('<|Merge Assistant Disable|>')
};
// 把“system:”在部分情况下替换为“user:”
// Replace 'system:' with 'user:' in some cases
content = preSystemToUserFallback(content, roleFeatures, mergeDisable);
// 开始合并 (首次)
// Start merging (first time)
content = xmlHyperMerge(content, roleFeatures, mergeDisable);
// 处理 <@N> 插入
// Process <@N> insertion
content = handleSubInsertion(content);
// 第2轮正则 (order=2)
// 2nd round regex (order=2)
[content, regexLogs] = xmlHyperRegex(content, 2, regexLogs);
// 第2次合并
// 2nd merge
content = xmlHyperMerge(content, roleFeatures, mergeDisable);
// 第3轮正则 (order=3)
// 3rd round regex (order=3)
[content, regexLogs] = xmlHyperRegex(content, 3, regexLogs);
// 插入对 <|padtxtX|> 的处理 or countTokens, etc.
// Insert handling for <|padtxtX|> or countTokens, etc.
// 收尾清理
// Final cleanup
content = finalizeCleanup(content);
return content.trim();
@@ -256,11 +257,11 @@ function xmlHyperProcess(originalContent, roleFeatures) {
/**
* xmlHyperRegex:
* 匹配 <regex order=?> " /pattern/flags ":" replacement" </regex>
* <regex order=?> " /pattern/flags ":" replacement" </regex>
*/
function xmlHyperRegex(original, order, logs) {
let content = original;
// 仅处理同 order 的 block
// Only process blocks with same order
const patternRegex = new RegExp(
`<regex(?: +order *= *(${order}))?>\\s*"\\/([^"]*?)\\/([gimsyu]*)"\\s*:\\s*"(.*?)"\\s*<\\/regex>`,
'gm'
@@ -276,10 +277,10 @@ function xmlHyperRegex(original, order, logs) {
logs += `${entire}\n`;
try {
const regObj = new RegExp(rawPattern, rawFlags);
replacement = JSON.parse(`"${replacement.replace(/\\?"/g, '\\"')}"`); // 反序列化
replacement = JSON.parse(`"${replacement.replace(/\\?"/g, '\\"')}"`); // Deserialize
content = content.replace(regObj, replacement);
} catch (err) {
console.warn(`Regex parse/replace error in block: ${entire}\n`, err);
sysLogger.warn(`Regex parse/replace error in block: ${entire}\n`, err);
}
}
return [content, logs];
@@ -287,26 +288,26 @@ function xmlHyperRegex(original, order, logs) {
/**
* content = content.replace(...)
* 而后 system: => user:
* 根据 roleFeatures.systemRole / userRole 动态替换
* system: => user:
* roleFeatures.systemRole / userRole
*/
function preSystemToUserFallback(content, roleFeatures, mergeDisable) {
if (mergeDisable.all || mergeDisable.system || mergeDisable.user) {
return content; // 不做任何替换
return content; // Do not replace
}
const systemName = stripPrefix(roleFeatures.systemRole, roleFeatures.prefix);
const userName = stripPrefix(roleFeatures.userRole, roleFeatures.prefix);
const assistantName = stripPrefix(roleFeatures.assistantRole, roleFeatures.prefix);
// 前面非 user|assistant 段落时的 system: -> 去掉
// Remove system: prefix when preceding non-user|assistant paragraphs
const re1 = new RegExp(
`(\\n\\n|^\\s*)(?<!\\n\\n(${userName}|${assistantName}):.*?)${systemName}:\\s*`,
'gs'
);
content = content.replace(re1, '$1');
// 补充 system => user
// Supplement system => user
const re2 = new RegExp(`(\\n\\n|^\\s*)${systemName}:\\s*`, 'g');
content = content.replace(re2, `\n\n${userName}: `);
@@ -315,31 +316,31 @@ function preSystemToUserFallback(content, roleFeatures, mergeDisable) {
/**
* xmlHyperMerge:
* 利用段落合并逻辑动态 roleFeatures
* roleFeatures
*/
function xmlHyperMerge(original, roleFeatures, mergeDisable) {
let content = original;
if (mergeDisable.all) {
return content; // 禁用合并
return content; // Disable merge
}
// 先获取名称
// First get name
const sys = stripPrefix(roleFeatures.systemRole, roleFeatures.prefix);
const usr = stripPrefix(roleFeatures.userRole, roleFeatures.prefix);
const ast = stripPrefix(roleFeatures.assistantRole, roleFeatures.prefix);
// 合并 system
// Merge system section
if (!mergeDisable.system) {
const regSys = new RegExp(`(?:\\n\\n|^\\s*)${escapeRegExp(sys)}:\\s*(.*?)(?=\\n\\n(?:${escapeRegExp(usr)}|${escapeRegExp(ast)}|$))`, 'gs');
content = content.replace(regSys, (_m, p1) => `\n\n${sys}: ${p1}`);
}
// 合并 user
// Merge user section
if (!mergeDisable.user) {
const regUsr = new RegExp(`(?:\\n\\n|^\\s*)${escapeRegExp(usr)}:\\s*(.*?)(?=\\n\\n(?:${escapeRegExp(ast)}|${escapeRegExp(sys)}|$))`, 'gs');
content = content.replace(regUsr, (_m, p1) => `\n\n${usr}: ${p1}`);
}
// 合并 assistant
// Merge assistant section
if (!mergeDisable.assistant) {
const regAst = new RegExp(`(?:\\n\\n|^\\s*)${escapeRegExp(ast)}:\\s*(.*?)(?=\\n\\n(?:${escapeRegExp(usr)}|${escapeRegExp(sys)}|$))`, 'gs');
content = content.replace(regAst, (_m, p1) => `\n\n${ast}: ${p1}`);
@@ -349,14 +350,14 @@ function xmlHyperMerge(original, roleFeatures, mergeDisable) {
}
/**
* splitContent = content.split(regExp)
* 然后 for each <@N> => 把里面内容插入到 倒数第N段落后面
* 删除 match[0]
* splitContent = content.split(regExp)
* for each <@N> => N
* match[0]
*/
function handleSubInsertion(original) {
let content = original;
// \n\n(?=.*?:) 拆分
// Split by \n\n(?=.*?:)
let splitted = content.split(/\n\n(?=.*?:)/g);
let match;
@@ -368,25 +369,25 @@ function handleSubInsertion(original) {
content = content.replace(match[0], '');
}
// 重组
// Reassemble
content = splitted.join('\n\n').replace(/<@(\d+)>.*?<\/@\1>/gs, '');
return content;
}
/**
* 移除 <regex> 统一换行<|curtail|> => 换行<|join|> =>
* <|space|> => ' '以及 <|xxx|> => JSON.parse
* 然后去除多余空行
* Remove <regex> blockUnify line breaks<|curtail|> => newline<|join|> =>
* <|space|> => ' ' <|xxx|> => JSON.parse
* Remove extra blank lines
*/
function finalizeCleanup(original) {
let content = original;
// 移除 <regex>
// Remove <regex> block
content = content.replace(/<regex( +order *= *\d)?>.*?<\/regex>/gm, '');
// 统一换行
// Unify line breaks
content = content.replace(/\r\n|\r/gm, '\n');
// <|curtail|> => 换行
// <|curtail|> => newline
content = content.replace(/\s*<\|curtail\|>\s*/g, '\n');
// <|join|> => ''
@@ -395,25 +396,25 @@ function finalizeCleanup(original) {
// <|space|> => ' '
content = content.replace(/\s*<\|space\|>\s*/g, ' ');
// JSON反序列化
// JSON deserialization
content = content.replace(/<\|(\\.*?)\|>/g, (m, p1) => {
try {
return JSON.parse(`"${p1.replace(/\\?"/g, '\\"')}"`);
} catch {
return m; // 保留原文本
return m; // Keep original text
}
});
// 移除其他 <|xxx|> 标记
// Remove other <|xxx|> tags
content = content.replace(/\s*<\|(?!padtxt).*?\|>\s*/g, '\n\n');
// 去除多余空行
// Remove extra blank lines
content = content.trim().replace(/(?<=\n)\n(?=\n)/g, '');
return content;
}
/** stripPrefix: 如果 role 中含有前缀(如 \u0008)去掉 */
/** stripPrefix: role ( \u0008) */
function stripPrefix(fullStr, prefix) {
if (!prefix) return fullStr;
if (fullStr.startsWith(prefix)) {
@@ -422,7 +423,7 @@ function stripPrefix(fullStr, prefix) {
return fullStr;
}
/** 转义正则特殊字符 */
/** */
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
+67
View File
@@ -0,0 +1,67 @@
import sysLogger from './utils/sysLogger.mjs';
const imageStorage = new Map();
let imageCounter = 0;
/**
*
* @param {string} base64Data base64
* @param {string} mediaType ("image/jpeg")
* @returns {{ imageId: string, mediaType: string }}
*/
export function storeImage(base64Data, mediaType) {
const randomPart = Math.floor(Math.random() * 10000);
const imageId = `image_${Date.now()}_${imageCounter++}_${randomPart}`;
imageStorage.set(imageId, { imageId, base64Data, mediaType });
// sysLogger.debug(`Image stored with ID: ${imageId}, Media Type: ${mediaType}`);
// Print stored base64
// sysLogger.debug(`Base64 Data for Image ID ${imageId}: ${base64Data.substring(0, 100)}...`);
return { imageId, mediaType };
}
/**
*
* @param {string} imageId ID
* @returns {object|null} base64
*/
export function getImage(imageId) {
return imageStorage.get(imageId) || null;
}
/**
*
* @returns {object|null} base64
*/
export function getLastImage() {
const lastKey = Array.from(imageStorage.keys()).pop();
return lastKey ? imageStorage.get(lastKey) : null;
}
/**
*
* @param {string} imageId ID
*/
export function removeImage(imageId) {
imageStorage.delete(imageId);
}
/**
*
*/
export function clearAllImages() {
imageStorage.clear();
imageCounter = 0;
// sysLogger.debug("All images have been cleared from memory.");
}
/**
*
*/
export function printAllImages() {
sysLogger.debug('Current images stored in memory:');
imageStorage.forEach((value, key) => {
sysLogger.debug(`Image ID: ${key}, Media Type: ${value.mediaType}`);
sysLogger.debug(`Base64 Data: ${value.base64Data.substring(0, 100)}...`);
});
}
+257 -152
View File
@@ -1,6 +1,8 @@
import fs from "fs";
import express from "express";
import { Semaphore, E_CANCELED } from "async-mutex";
import {createEvent, getGitRevision} from "./utils/cookieUtils.mjs";
import YouProvider from "./provider.mjs";
import YouProvider from "./you_providers/youProvider.mjs";
import localtunnel from "localtunnel";
import ngrok from 'ngrok';
import {v4 as uuidv4} from "uuid";
@@ -10,41 +12,38 @@ import fetch from 'node-fetch';
import path from 'path';
import geoip from 'geoip-lite';
import RequestLogger from './requestLogger.mjs';
import sysLogger from './utils/sysLogger.mjs';
const requestSemaphore = new Semaphore(1, { maxQueue: 3 });
// Per-request client connection state (replaces previous global singleton)
class ClientState {
#closed = false;
setClosed(value) {
this.#closed = Boolean(value);
}
isClosed() {
return this.#closed;
}
}
const app = express();
const port = process.env.PORT || 8080;
const validApiKey = process.env.PASSWORD;
const availableModels = [
"openai_o3_mini_high",
"openai_o3_mini_medium",
"openai_o1",
"openai_o1_preview",
"gpt_4_5_preview",
"gpt_4o",
"gpt_4_turbo",
"gpt_4",
"claude_3_7_sonnet",
"claude_3_7_sonnet_thinking",
"claude_3_5_sonnet",
"claude_3_opus",
"claude_3_sonnet",
"claude_3_haiku",
"claude_2",
"llama3",
"gemini_pro",
"gemini_1_5_pro",
"gemini_1_5_flash",
"gemini_2_5_pro_experimental",
"databricks_dbrx_instruct",
"command_r",
"command_r_plus",
"zephyr",
"qwen2p5_72b",
"llama3_1_405b",
"grok_2",
"deepseek_r1",
"deepseek_v3"
let availableModels = [
"gemini_3_1_pro",
"claude_4_6_opus",
"claude_4_7_opus",
"claude_4_8_opus",
"claude_4_6_sonnet",
"gpt_5_5_thinking"
];
if (process.env.CUSTOM_MODELS) {
availableModels = process.env.CUSTOM_MODELS.split(",").map(m => m.trim());
}
const modelMappping = {
"claude-3-7-sonnet-latest": "claude_3_7_sonnet_thinking",
"claude-3-7-sonnet-20250219": "claude_3_7_sonnet",
@@ -62,27 +61,34 @@ const modelMappping = {
"gpt-4-turbo": "gpt_4_turbo",
"openai-o1": "openai_o1",
"o1-preview": "openai_o1",
"claude-4.6-sonnet": "claude_4_6_sonnet",
"claude-4.6-sonnet-thinking": "claude_4_6_sonnet_thinking",
"gpt-5.5-thinking": "gpt_5_5_thinking"
};
// import config.mjs
let config;
try {
const configModule = await import("./config.mjs");
const configModule = await import("../config.mjs");
config = configModule.config;
} catch (e) {
console.error(e);
console.error("config.mjs 不存在或者有错误,请检查");
sysLogger.error(e);
sysLogger.error("config.mjs does not exist or has errors, please check");
process.exit(1);
}
const provider = new YouProvider(config);
await provider.init(config);
// 初始化 SessionManager
const sessionManager = provider.getSessionManager();
const sessionManager = provider.sessionManager;
// 初始化 RequestLogger
// Initialize RequestLogger
const requestLogger = new RequestLogger();
// Global request logger
app.use((req, res, next) => {
sysLogger.debug(`[NETWORK] Received ${req.method} request to ${req.originalUrl}`);
next();
});
// handle preflight request
app.use((req, res, next) => {
@@ -114,62 +120,94 @@ app.get("/v1/models", OpenAIApiKeyAuth, (req, res) => {
});
// handle openai format model request
app.post("/v1/chat/completions", OpenAIApiKeyAuth, (req, res) => {
// 用于存储请求体
req.rawBody = "";
req.setEncoding("utf8");
clientState.setClosed(false);
const clientState = new ClientState();
// 接收数据
// Receive data
let bodySize = 0;
req.on("data", function (chunk) {
bodySize += chunk.length;
if (bodySize > 50 * 1024 * 1024) {
res.status(413).json({error: {code: 413, message: "Payload Too Large"}});
req.connection.destroy();
return;
}
req.rawBody += chunk;
});
// 数据接收完毕后处理请求
// Process request after data is received
req.on("end", async () => {
console.log("处理 OpenAI 格式的请求");
res.setHeader("Content-Type", "text/event-stream;charset=utf-8");
res.setHeader("Access-Control-Allow-Origin", "*");
let release;
try {
release = await requestSemaphore.acquire();
} catch (err) {
if (err === E_CANCELED || (err.message && err.message.includes('Queue is full'))) {
res.status(429).json({ error: "Too Many Requests. Queue is full." }).end();
return;
}
sysLogger.error("Semaphore acquire error:", err);
res.status(500).json({ error: "Internal Server Error" }).end();
return;
}
try {
sysLogger.debug("Handling OpenAI format request");
let jsonBody;
try {
sysLogger.debug("\n=== DEBUG: RAW REQUEST BODY ===");
sysLogger.debug(req.rawBody);
sysLogger.debug("===============================\n");
jsonBody = JSON.parse(req.rawBody);
} catch (error) {
res.setHeader("Content-Type", "application/json");
res.status(400).json({error: {code: 400, message: "Invalid JSON"}});
return;
}
// 规范化消息
if (jsonBody.stream) {
res.setHeader("Content-Type", "text/event-stream;charset=utf-8");
} else {
res.setHeader("Content-Type", "application/json;charset=utf-8");
}
res.setHeader("Access-Control-Allow-Origin", "*");
// Normalize message
jsonBody.messages = await openaiNormalizeMessages(jsonBody.messages);
console.log("message length: " + jsonBody.messages.length);
sysLogger.debug("message length: " + jsonBody.messages.length);
// 尝试映射模型
// Try to map model
if (jsonBody.model && modelMappping[jsonBody.model]) {
jsonBody.model = modelMappping[jsonBody.model];
}
if (jsonBody.model && !availableModels.includes(jsonBody.model)) {
res.json({error: {code: 404, message: "Invalid Model"}});
// If CUSTOM_MODELS is explicitly set, enforce the whitelist
if (process.env.CUSTOM_MODELS && !availableModels.includes(jsonBody.model)) {
res.status(404).json({error: {code: 404, message: "Invalid Model. This server restricts usage to: " + availableModels.join(", ")}});
return;
}
console.log("Using model " + jsonBody.model);
sysLogger.debug("Using model " + jsonBody.model);
let selectedSession;
let releaseSessionCalled = false;
let completion;
let cancel;
let selectedBrowserId;
// 定义释放会话
// Define Releasing session
const releaseSession = () => {
if (selectedSession && selectedBrowserId && !releaseSessionCalled) {
sessionManager.releaseSession(selectedSession, selectedBrowserId);
console.log(`释放会话 ${selectedSession} 和浏览器实例 ${selectedBrowserId}`);
sysLogger.debug(`Releasing session ${selectedSession} and browser instance ${selectedBrowserId}`);
releaseSessionCalled = true;
}
};
// 监听客户端关闭事件
// Listen to client close event
res.on("close", () => {
console.log(" > [Client closed]");
sysLogger.debug(" > [Client closed]");
clientState.setClosed(true);
if (completion) {
completion.removeAllListeners();
@@ -181,13 +219,13 @@ app.post("/v1/chat/completions", OpenAIApiKeyAuth, (req, res) => {
});
try {
// 获取客户端 IP
// Get client IP
const clientIpAddress = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
const geo = geoip.lookup(clientIpAddress) || {};
const locationInfo = `${geo.country || 'Unknown'}-${geo.region || 'Unknown'}-${geo.city || 'Unknown'}`;
const requestTime = new Date();
// 获取并锁定可用会话和浏览器实例
// Acquire and lock available session and browser instance
const {
selectedUsername,
modeSwitched,
@@ -195,9 +233,9 @@ app.post("/v1/chat/completions", OpenAIApiKeyAuth, (req, res) => {
} = await sessionManager.getSessionByStrategy('round_robin');
selectedSession = selectedUsername;
selectedBrowserId = browserInstance.id;
console.log("Using session " + selectedSession);
sysLogger.debug("Using session " + selectedSession);
// 记录请求信息
// Record request info
await requestLogger.logRequest({
time: requestTime,
ip: clientIpAddress,
@@ -213,13 +251,14 @@ app.post("/v1/chat/completions", OpenAIApiKeyAuth, (req, res) => {
stream: !!jsonBody.stream,
proxyModel: jsonBody.model,
useCustomMode: process.env.USE_CUSTOM_MODE === "true",
modeSwitched: modeSwitched // 传递模式切换标志
modeSwitched: modeSwitched,
clientState: clientState,
}));
// 监听开始事件
// Listen to start event
completion.on("start", (id) => {
if (jsonBody.stream) {
// 发送消息开始
// Start sending message
res.write(createEvent(":", "queue heartbeat 114514"));
res.write(
createEvent("data", {
@@ -239,10 +278,10 @@ app.post("/v1/chat/completions", OpenAIApiKeyAuth, (req, res) => {
}
});
// 监听完成事件
// Listen to completion event
completion.on("completion", (id, text) => {
if (jsonBody.stream) {
// 发送消息增量
// Send message delta
res.write(
createEvent("data", {
choices: [
@@ -266,9 +305,8 @@ app.post("/v1/chat/completions", OpenAIApiKeyAuth, (req, res) => {
})
);
} else {
// 只发送一次,发送最终响应
res.write(
JSON.stringify({
// Send only once, send final response
res.json({
id: id,
object: "chat.completion",
created: Math.floor(new Date().getTime() / 1000),
@@ -290,14 +328,12 @@ app.post("/v1/chat/completions", OpenAIApiKeyAuth, (req, res) => {
completion_tokens: 1,
total_tokens: 1,
},
})
);
res.end();
});
releaseSession();
}
});
// 监听结束事件
// Listen to end event
completion.on("end", () => {
if (jsonBody.stream) {
res.write(createEvent("data", "[DONE]"));
@@ -307,9 +343,9 @@ app.post("/v1/chat/completions", OpenAIApiKeyAuth, (req, res) => {
releaseSession();
});
// 监听错误事件
// Listen to error event
completion.on("error", (err) => {
console.error("Completion error:", err);
sysLogger.error("Completion error:", err);
const errorMessage = "Error occurred: " + (err.message || "Unknown error");
if (!res.headersSent) {
if (jsonBody.stream) {
@@ -370,10 +406,10 @@ app.post("/v1/chat/completions", OpenAIApiKeyAuth, (req, res) => {
});
} catch (error) {
console.error("Request error:", error);
sysLogger.error("Request error:", error);
releaseSession();
const errorMessage = "Error occurred, please check the log.\n\n出现错误,请检查日志:<pre>" + (error.stack || error) + "</pre>";
const errorMessage = "Error occurred, please check the log.\n\n<pre>" + (error.stack || error) + "</pre>";
if (!res.headersSent) {
if (jsonBody.stream) {
res.write(
@@ -430,6 +466,13 @@ app.post("/v1/chat/completions", OpenAIApiKeyAuth, (req, res) => {
}
}
}
} finally {
try {
if (release) release();
} catch (relErr) {
sysLogger.error("Semaphore release error:", relErr);
}
}
});
});
@@ -451,25 +494,25 @@ async function openaiNormalizeMessages(messages) {
currentSystemMessage = "";
}
// 检查消息内容
// Check message content
if (Array.isArray(message.content)) {
const textContent = message.content
.filter(item => item.type === 'text')
.map(item => item.text)
.join('\n');
// 处理图片内容,存储图片
// Process image content, store image
for (const item of message.content) {
if (item.type === 'image_url' && item.image_url?.url) {
// 获取媒体类型
// Get media type
const mediaType = await getMediaTypeFromUrl(item.image_url.url);
// 获取图片 base64
// Get image base64
const base64Data = await fetchImageAsBase64(item.image_url.url);
if (base64Data) {
const {imageId} = storeImage(base64Data, mediaType);
console.log(`Image stored with ID: ${imageId}, Media Type: ${mediaType}`);
sysLogger.debug(`Image stored with ID: ${imageId}, Media Type: ${mediaType}`);
} else {
console.warn('Failed to store image due to missing data.');
sysLogger.warn('Failed to store image due to missing data.');
}
}
}
@@ -478,7 +521,7 @@ async function openaiNormalizeMessages(messages) {
} else if (typeof message.content === 'string') {
normalizedMessages.push(message);
} else {
console.warn('未知的消息内容格式:', message.content);
sysLogger.warn('Unknown message content format:', message.content);
normalizedMessages.push(message);
}
}
@@ -491,14 +534,14 @@ async function openaiNormalizeMessages(messages) {
return normalizedMessages;
}
// 图片 URL 获取媒体类型
// Get media type from image URL
async function getMediaTypeFromUrl(url) {
try {
const response = await fetch(url, {method: 'HEAD'});
const contentType = response.headers.get('content-type');
return contentType || guessMediaTypeFromUrl(url);
} catch (error) {
console.warn('无法获取媒体类型,尝试根据 URL 推断', error);
sysLogger.warn('Unable to get media type, trying to infer from URL', error);
return guessMediaTypeFromUrl(url);
}
}
@@ -518,7 +561,7 @@ function guessMediaTypeFromUrl(url) {
}
}
// 图片 URL 获取 base64
// Get base64 from image URL
async function fetchImageAsBase64(url) {
try {
const response = await fetch(url);
@@ -526,7 +569,7 @@ async function fetchImageAsBase64(url) {
const buffer = Buffer.from(arrayBuffer);
return buffer.toString('base64');
} catch (error) {
console.error('Failed to fetch image data:', error);
sysLogger.error('Failed to fetch image data:', error);
return null;
}
}
@@ -536,14 +579,35 @@ async function fetchImageAsBase64(url) {
app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => {
req.rawBody = "";
req.setEncoding("utf8");
clientState.setClosed(false);
const clientState = new ClientState();
let bodySize = 0;
req.on("data", function (chunk) {
bodySize += chunk.length;
if (bodySize > 50 * 1024 * 1024) {
res.status(413).json({error: {code: 413, message: "Payload Too Large"}});
req.connection.destroy();
return;
}
req.rawBody += chunk;
});
req.on("end", async () => {
console.log("处理 Anthropic 格式的请求");
let release;
try {
release = await requestSemaphore.acquire();
} catch (err) {
if (err === E_CANCELED || (err.message && err.message.includes('Queue is full'))) {
res.status(429).json({ error: "Too Many Requests. Queue is full." }).end();
return;
}
sysLogger.error("Semaphore acquire error:", err);
res.status(500).json({ error: "Internal Server Error" }).end();
return;
}
try {
sysLogger.debug("Handle Anthropic format request");
res.setHeader("Content-Type", "text/event-stream;charset=utf-8");
res.setHeader("Access-Control-Allow-Origin", "*");
let jsonBody;
@@ -555,14 +619,14 @@ app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => {
return;
}
// 处理消息格式
// Process message format
jsonBody.messages = anthropicNormalizeMessages(jsonBody.messages);
if (jsonBody.system) {
// 把系统消息加入 messages 的首条
// System messages
jsonBody.messages.unshift({role: "system", content: jsonBody.system});
}
console.log("message length:" + jsonBody.messages.length);
sysLogger.debug("message length:" + jsonBody.messages.length);
// decide which model to use
let proxyModel;
@@ -575,7 +639,7 @@ app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => {
} else {
proxyModel = "claude_3_opus";
}
console.log(`Using model ${proxyModel}`);
sysLogger.info(`Using model ${proxyModel}`);
if (proxyModel && !availableModels.includes(proxyModel)) {
res.json({error: {code: 404, message: "Invalid Model"}});
@@ -587,18 +651,18 @@ app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => {
let completion;
let cancel;
let selectedBrowserId;
// 定义释放会话
// Define Releasing session
const releaseSession = () => {
if (selectedSession && selectedBrowserId && !releaseSessionCalled) {
sessionManager.releaseSession(selectedSession, selectedBrowserId);
console.log(`释放会话 ${selectedSession} 和浏览器实例 ${selectedBrowserId}`);
sysLogger.debug(`Releasing session ${selectedSession} and browser instance ${selectedBrowserId}`);
releaseSessionCalled = true;
}
};
// 监听客户端关闭事件
// Listen to client close event
res.on("close", () => {
console.log(" > [Client closed]");
sysLogger.debug(" > [Client closed]");
clientState.setClosed(true);
if (completion) {
completion.removeAllListeners();
@@ -610,13 +674,13 @@ app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => {
});
try {
// 获取客户端 IP
// Get client IP
const clientIpAddress = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
const geo = geoip.lookup(clientIpAddress) || {};
const locationInfo = `${geo.country || 'Unknown'}-${geo.region || 'Unknown'}-${geo.city || 'Unknown'}`;
const requestTime = new Date();
// 获取并锁定可用会话和浏览器实例
// Acquire and lock available session and browser instance
const {
selectedUsername,
modeSwitched,
@@ -624,9 +688,9 @@ app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => {
} = await sessionManager.getSessionByStrategy('round_robin');
selectedSession = selectedUsername;
selectedBrowserId = browserInstance.id;
console.log("Using session " + selectedSession);
sysLogger.debug("Using session " + selectedSession);
// 记录请求信息
// Record request info
await requestLogger.logRequest({
time: requestTime,
ip: clientIpAddress,
@@ -642,10 +706,11 @@ app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => {
stream: !!jsonBody.stream,
proxyModel: proxyModel,
useCustomMode: process.env.USE_CUSTOM_MODE === "true",
modeSwitched: modeSwitched // 传递模式切换标志
modeSwitched: modeSwitched,
clientState: clientState,
}));
// 监听开始事件
// Listen to start event
completion.on("start", (id) => {
if (jsonBody.stream) {
// send message start
@@ -671,7 +736,7 @@ app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => {
}
});
// 监听完成事件
// Listen to completion event
completion.on("completion", (id, text) => {
if (jsonBody.stream) {
// send message delta
@@ -681,7 +746,7 @@ app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => {
delta: {type: "text_delta", text: text},
}));
} else {
// 只会发一次,发送final response
// Will only send once, sending final response
res.write(JSON.stringify({
id: id,
content: [
@@ -698,7 +763,7 @@ app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => {
}
});
// 监听结束事件
// Listen to end event
completion.on("end", () => {
if (jsonBody.stream) {
res.write(createEvent("content_block_stop", {type: "content_block_stop", index: 0}));
@@ -713,10 +778,10 @@ app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => {
releaseSession();
});
// 监听错误事件
// Listen to error event
completion.on("error", (err) => {
console.error("Completion error:", err);
// 向客户端返回错误信息
sysLogger.error("Completion error:", err);
// Return error message to client
const errorMessage = "Error occurred: " + (err.message || "Unknown error");
if (!res.headersSent) {
if (jsonBody.stream) {
@@ -742,10 +807,10 @@ app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => {
});
} catch (error) {
console.error("Request error:", error);
sysLogger.error("Request error:", error);
releaseSession();
const errorMessage = "Error occurred, please check the log.\n\n出现错误,请检查日志:<pre>" + (error.stack || error) + "</pre>";
const errorMessage = "Error occurred, please check the log.\n\n<pre>" + (error.stack || error) + "</pre>";
if (!res.headersSent) {
if (jsonBody.stream) {
res.write(createEvent("content_block_delta", {
@@ -767,33 +832,40 @@ app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => {
}
}
}
} finally {
try {
if (release) release();
} catch (relErr) {
sysLogger.error("Semaphore release error:", relErr);
}
}
});
});
// 辅助函数:规范化消息格式
// Helper function: normalize message format
function anthropicNormalizeMessages(messages) {
return messages.map(message => {
if (typeof message.content === 'string') {
return message;
} else if (Array.isArray(message.content)) {
// 提取文本内容
// Extract text content
const textContent = message.content
.filter(item => item.type === 'text')
.map(item => item.text)
.join('\n');
// 处理图片内容,存储图片
// Process image and document content, store file
message.content.forEach(item => {
if (item.type === 'image' && item.source?.type === 'base64') {
if ((item.type === 'image' || item.type === 'document') && item.source?.type === 'base64') {
const {imageId, mediaType} = storeImage(item.source.data, item.source.media_type);
console.log(`Image stored with ID: ${imageId}, Media Type: ${mediaType}`);
sysLogger.debug(`File stored with ID: ${imageId}, Media Type: ${mediaType}`);
}
});
return {...message, content: textContent};
} else {
console.warn('Unknown message format:', message);
return message; // 未知格式,返回原始消息
sysLogger.warn('Unknown message format:', message);
return message; // Unknown format, returning original message
}
});
}
@@ -803,7 +875,7 @@ function anthropicNormalizeMessages(messages) {
app.use((req, res, next) => {
const {revision, branch} = getGitRevision();
res.status(404).send("Not Found (YouChat_Proxy " + revision + "@" + branch + ")");
console.log("收到了错误路径的请求,请检查您使用的API端点是否正确。")
sysLogger.debug("Received request on wrong path, please check if your API endpoint is correct.")
});
const createLocaltunnel = async (port, subdomain) => {
@@ -814,20 +886,20 @@ const createLocaltunnel = async (port, subdomain) => {
try {
const tunnel = await localtunnel(tunnelOptions);
console.log(`隧道已成功创建,可通过以下URL访问: ${tunnel.url}/v1`);
tunnel.on("close", () => console.log("已关闭隧道"));
sysLogger.info(`Tunnel successfully created, access via: ${tunnel.url}/v1`);
tunnel.on("close", () => sysLogger.info("Closed tunnel"));
return tunnel;
} catch (error) {
console.error("创建localtunnel隧道失败:", error);
sysLogger.error("Failed to create localtunnel tunnel:", error);
}
};
/*
* 创建ngrok隧道
* @param {number} port - 本地端口
* @param {string} authToken - ngrok的认证token
* @param {string} customDomain - 自定义域名
* @param {string} subdomain - 子域名
* Createngrok
* @param {number} port -
* @param {string} authToken - ngroktoken
* @param {string} customDomain -
* @param {string} subdomain -
*/
const createNgrok = async (port, authToken, customDomain, subdomain) => {
const ngrokOptions = {addr: port, authtoken: authToken};
@@ -845,14 +917,14 @@ const createNgrok = async (port, authToken, customDomain, subdomain) => {
try {
const url = await ngrok.connect(ngrokOptions);
console.log(`隧道已成功创建,可通过以下URL访问: ${url}/v1`);
sysLogger.debug(`CreateURL: ${url}/v1`);
process.on('SIGTERM', async () => {
await ngrok.kill();
console.log("已关闭隧道");
sysLogger.info("Closed tunnel");
});
return url;
} catch (error) {
console.error("创建ngrok隧道失败:", error);
sysLogger.error("Failed to create ngrok tunnel:", error);
} finally {
if (originalHttpProxy) process.env.HTTP_PROXY = originalHttpProxy;
if (originalHttpsProxy) process.env.HTTPS_PROXY = originalHttpsProxy;
@@ -860,7 +932,7 @@ const createNgrok = async (port, authToken, customDomain, subdomain) => {
};
const createTunnel = async (tunnelType, port) => {
console.log(`创建${tunnelType}隧道中...`);
sysLogger.info(`Creating ${tunnelType} tunnel...`);
if (tunnelType === "localtunnel") {
return createLocaltunnel(port, process.env.SUBDOMAIN);
} else if (tunnelType === "ngrok") {
@@ -868,15 +940,18 @@ const createTunnel = async (tunnelType, port) => {
}
};
app.listen(port, async () => {
// 输出当前月份的请求统计信息
provider.getLogger().printStatistics();
console.log(`YouChat proxy listening on port ${port}`);
app.get('/health', (req, res) => {
res.status(200).json({ status: "ok" });
});
const server = app.listen(port, async () => {
provider.logger.printStatistics();
sysLogger.info(`YouChat proxy listening on port ${port}`);
if (!validApiKey) {
console.log(`Proxy is currently running with no authentication`);
sysLogger.info(`Proxy is currently running with no authentication`);
}
console.log(`Custom mode: ${process.env.USE_CUSTOM_MODE === "true" ? "enabled" : "disabled"}`);
console.log(`Mode rotation: ${process.env.ENABLE_MODE_ROTATION === "true" ? "enabled" : "disabled"}`);
sysLogger.info(`Custom mode: ${process.env.USE_CUSTOM_MODE === "true" ? "enabled" : "disabled"}`);
sysLogger.debug(`Mode rotation: ${process.env.ENABLE_MODE_ROTATION === "true" ? "enabled" : "disabled"}`);
if (process.env.ENABLE_TUNNEL === "true") {
const tunnelType = process.env.TUNNEL_TYPE || "localtunnel";
@@ -884,13 +959,55 @@ app.listen(port, async () => {
}
});
// ── Graceful shutdown ──────────────────────────────────────────────────
async function gracefulShutdown(signal) {
sysLogger.debug(`\n${signal} received. Shutting down gracefully...`);
// Stop accepting new connections
server.close(() => {
sysLogger.info('HTTP server closed');
});
// Close browser instances
try {
const browserInstances = sessionManager.browserInstances || [];
for (const instance of browserInstances) {
try {
await instance.browser.close();
sysLogger.debug(`Closed browser instance: ${instance.id}`);
} catch (e) {
sysLogger.warn(`Error closing browser ${instance.id}:`, e.message);
}
}
} catch (e) {
sysLogger.warn('Error closing browsers:', e.message);
}
// Stop network monitor
try {
const networkMonitor = provider?.networkMonitor;
if (networkMonitor) {
networkMonitor.stopMonitoring();
sysLogger.debug('Network monitor stopped');
}
} catch (e) {
// Ignore
}
sysLogger.debug('Shutdown complete');
process.exit(0);
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
function AnthropicApiKeyAuth(req, res, next) {
const reqApiKey = req.header("x-api-key");
if (validApiKey && reqApiKey !== validApiKey) {
// If Environment variable PASSWORD is set AND x-api-key header is not equal to it, return 401
const clientIpAddress = req.headers["x-forwarded-for"] || req.ip;
console.log(`Receviced Request from IP ${clientIpAddress} but got invalid password.`);
sysLogger.debug(`Receviced Request from IP ${clientIpAddress} but got invalid password.`);
return res.status(401).json({error: "Invalid Password"});
}
@@ -903,24 +1020,12 @@ function OpenAIApiKeyAuth(req, res, next) {
if (validApiKey && reqApiKey !== "Bearer " + validApiKey) {
// If Environment variable PASSWORD is set AND Authorization header is not equal to it, return 401
const clientIpAddress = req.headers["x-forwarded-for"] || req.ip;
console.log(`Receviced Request from IP ${clientIpAddress} but got invalid password.`);
sysLogger.debug(`Receviced Request from IP ${clientIpAddress} but got invalid password.`);
return res.status(401).json({error: {code: 403, message: "Invalid Password"}});
}
next();
}
// Path: cookieUtils.mjs
class ClientState {
#closed = false;
setClosed(value) {
this.#closed = Boolean(value);
}
isClosed() {
return this.#closed;
}
}
export const clientState = new ClientState();
// ClientState class is now defined at the top of this file and instantiated per-request.
// No global singleton export needed.
@@ -1,5 +1,6 @@
import dns from 'dns';
import { EventEmitter } from 'events';
import sysLogger from './utils/sysLogger.mjs';
class NetworkMonitor extends EventEmitter {
constructor() {
@@ -21,17 +22,17 @@ class NetworkMonitor extends EventEmitter {
}
async startMonitoring() {
console.log("开始网络监控...");
sysLogger.debug("Starting network monitor...");
this.checkInterval = setInterval(async () => {
const isConnected = await this.checkConnection();
if (!isConnected && !this.isBlocked) {
this.isBlocked = true;
this.emit('networkDown');
console.log("检测到网络异常");
sysLogger.debug("Network anomaly detected");
} else if (isConnected && this.isBlocked) {
this.isBlocked = false;
this.emit('networkUp');
console.log("网络恢复正常");
sysLogger.debug("Network recovered");
}
}, 5000);
}
+11 -10
View File
@@ -3,6 +3,7 @@ import HttpsProxyAgent from 'https-proxy-agent';
import { URL } from 'url';
import http from 'http';
import https from 'https';
import sysLogger from './utils/sysLogger.mjs';
let globalProxyAgent = null;
@@ -35,7 +36,7 @@ function parseProxyUrl(proxyUrl) {
return { protocol: protocol.replace(':', ''), host, port, username, password };
} catch (error) {
console.error(`Invalid proxy URL: ${proxyUrl}`);
sysLogger.error(`Invalid proxy URL: ${proxyUrl}`);
return null;
}
}
@@ -43,17 +44,17 @@ function parseProxyUrl(proxyUrl) {
function createProxyAgent() {
const proxyUrl = getProxyUrl();
if (!proxyUrl) {
console.log('Proxy environment variable not set, will not use proxy.');
sysLogger.info('Proxy environment variable not set, will not use proxy.');
return null;
}
const parsedProxy = parseProxyUrl(proxyUrl);
if (!parsedProxy) return null;
console.log(`Using proxy: ${proxyUrl}`);
sysLogger.info(`Using proxy: ${proxyUrl}`);
if (parsedProxy.protocol === 'socks5') {
console.log('Using SOCKS5 proxy');
sysLogger.info('Using SOCKS5 proxy');
return new SocksProxyAgent({
hostname: parsedProxy.host,
port: parsedProxy.port,
@@ -62,7 +63,7 @@ function createProxyAgent() {
protocol: 'socks5:'
});
} else {
console.log('Using HTTP/HTTPS proxy');
sysLogger.info('Using HTTP/HTTPS proxy');
return new HttpsProxyAgent.HttpsProxyAgent(proxyUrl);
}
}
@@ -72,7 +73,7 @@ export function setGlobalProxy() {
if (proxyUrl) {
globalProxyAgent = createProxyAgent();
// 重写 http https 模块的 request 方法
// Rewrite request method of http and https modules
const originalHttpRequest = http.request;
const originalHttpsRequest = https.request;
@@ -92,9 +93,9 @@ export function setGlobalProxy() {
return originalHttpsRequest.call(this, options, callback);
};
console.log(`Global proxy set to: ${proxyUrl}`);
sysLogger.info(`Global proxy set to: ${proxyUrl}`);
} else {
console.log('No proxy environment variable set, global proxy not configured.');
sysLogger.info('No proxy environment variable set, global proxy not configured.');
}
}
@@ -103,9 +104,9 @@ export function setProxyEnvironmentVariables() {
if (proxyUrl) {
process.env.HTTP_PROXY = proxyUrl;
process.env.HTTPS_PROXY = proxyUrl;
console.log(`Set proxy environment variables to: ${proxyUrl}`);
sysLogger.info(`Set proxy environment variables to: ${proxyUrl}`);
}
}
// 全局代理
// Global proxy
setGlobalProxy();
+84 -101
View File
@@ -1,86 +1,65 @@
import fs from 'fs';
import path from 'path';
import {Mutex} from 'async-mutex';
import winston from 'winston';
// 请求日志记录
const logFilePath = path.join(process.cwd(), 'request_logs.log');
/*
* 黑名单文件格式
* {
* "ip1": {
* "permanent": true
* },
* "ip2": {
* "permanent": true
* }
* ...
*/
const blacklistFilePath = path.join(process.cwd(), 'ip_blacklist.json');
/*
* 临时限制文件格式
* {
* "ip1": 1630000000000,
* "ip2": 1630000000000
* ...
* }
*/
const tempLimitFilePath = path.join(process.cwd(), 'temp_limits.json');
// 是否启用检测
import sysLogger from './utils/sysLogger.mjs';
// Logging
const logFilePath = path.join(process.cwd(), 'data', 'request_logs.log');
const blacklistFilePath = path.join(process.cwd(), 'data', 'ip_blacklist.json');
const tempLimitFilePath = path.join(process.cwd(), 'data', 'temp_limits.json');
const ENABLE_DETECTION = process.env.ENABLE_DETECTION !== 'false';
class RequestLogger {
constructor() {
// 日志记录
this.logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({format: 'YYYY年MM月DD日 HH时mm分ss秒'}),
winston.format.printf(({timestamp, level, message}) => {
return `${timestamp} | ${message}`;
})
),
transports: [
new winston.transports.File({filename: logFilePath})
]
});
// Logging
this.logger = {
info: async (message) => {
const timestamp = new Date().toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
}).replace(/\//g, '-');
const logLine = `${timestamp} | ${message}\n`;
try {
await fs.promises.appendFile(logFilePath, logLine);
} catch (e) {
sysLogger.error('Failed to write to request log:', e.message);
}
}
};
try {
if (!fs.existsSync(blacklistFilePath)) {
fs.writeFileSync(blacklistFilePath, JSON.stringify({}), 'utf8');
}
} catch (err) {
console.error(`初始化黑名单文件出错:${err.message}`);
sysLogger.error(`Error initializing blacklist file: ${err.message}`);
}
this.ipRequestRecords = {}; // 每个IP的请求时间记录
this.ip30sTriggerCount = {}; // 统计每个IP触发30秒限制的次数
this.temporaryLimits = {}; // 每个IP的临时限制到期时间
this.ipMutexes = {}; // 为每个IP分配一个独立的Mutex
this.blacklistMutex = new Mutex(); // 黑名单文件的Mutex
this.ipRequestRecords = {}; // Request time record per IP
this.ip30sTriggerCount = {}; // Count 30s limit triggers for each IP
this.temporaryLimits = {}; // Temp limit expiration time per IP
this.ipMutexes = {}; // Assign an independent Mutex for each IP
this.blacklistMutex = new Mutex(); // Blacklist file Mutex
try {
if (!fs.existsSync(tempLimitFilePath)) {
fs.writeFileSync(tempLimitFilePath, JSON.stringify({}), 'utf8');
}
} catch (err) {
console.error(`初始化临时限制文件出错:${err.message}`);
sysLogger.error(`Error initializing temp limit file: ${err.message}`);
}
// 加载临时限制
// Load temp limits
this.loadTempLimitsFromFile();
// 监听文件变化
// Listen to file changes
this.setupTempLimitsFileWatcher();
// 清理无活动IP
// Clean inactive IPs
setInterval(() => {
this.cleanUpInactiveIPs();
}, 60 * 60 * 1000);
}
// 获取IPMutex
// Get IP Mutex
getMutexForIP(ip) {
if (!this.ipMutexes[ip]) {
this.ipMutexes[ip] = new Mutex();
@@ -88,17 +67,17 @@ class RequestLogger {
return this.ipMutexes[ip];
}
// 从文件加载临时限制
loadTempLimitsFromFile() {
// Load temporary limits from file
async loadTempLimitsFromFile() {
try {
const dataRaw = fs.readFileSync(tempLimitFilePath, 'utf8');
const dataRaw = await fs.promises.readFile(tempLimitFilePath, 'utf8');
if (!dataRaw.trim()) {
// 文件为空,则 no-op
// File is empty, no-op
return;
}
const parsed = JSON.parse(dataRaw);
const now = Date.now();
// 清空内存
// Clear memory
this.temporaryLimits = {};
for (const ip in parsed) {
const expireTime = parseInt(parsed[ip], 10);
@@ -106,28 +85,28 @@ class RequestLogger {
this.temporaryLimits[ip] = expireTime;
}
}
this.saveTempLimitsToFile();
await this.saveTempLimitsToFile();
} catch (error) {
console.error(`读取临时限制文件出错:${error.message}`);
sysLogger.error(`Error reading temp limit file: ${error.message}`);
}
}
saveTempLimitsToFile() {
async saveTempLimitsToFile() {
try {
const now = Date.now();
// 移除过期记录
// Remove expired records
for (const ip in this.temporaryLimits) {
if (this.temporaryLimits[ip] <= now) {
delete this.temporaryLimits[ip];
}
}
fs.writeFileSync(
await fs.promises.writeFile(
tempLimitFilePath,
JSON.stringify(this.temporaryLimits, null, 4),
'utf8'
);
} catch (error) {
console.error(`写入临时限制文件出错:${error.message}`);
sysLogger.error(`Error writing temp limit file: ${error.message}`);
}
}
@@ -139,61 +118,65 @@ class RequestLogger {
}
});
} catch (err) {
console.error(`监听临时限制文件出错:${err.message}`);
sysLogger.error(`Error listening to temp limit file: ${err.message}`);
}
}
// 检查是否在黑名单
// Check if in blacklist
async isBlacklisted(ip) {
return await this.blacklistMutex.runExclusive(async () => {
try {
if (!fs.existsSync(blacklistFilePath)) {
fs.writeFileSync(blacklistFilePath, JSON.stringify({}), 'utf8');
try {
await fs.promises.access(blacklistFilePath, fs.constants.F_OK);
} catch (err) {
await fs.promises.writeFile(blacklistFilePath, JSON.stringify({}), 'utf8');
}
const blacklistData = fs.readFileSync(blacklistFilePath, 'utf8');
const blacklistData = await fs.promises.readFile(blacklistFilePath, 'utf8');
const blacklist = JSON.parse(blacklistData || '{}');
const info = blacklist[ip];
if (info && info.permanent) {
return true;
} else if (info) {
// 移除非法
// Remove invalid
delete blacklist[ip];
fs.writeFileSync(blacklistFilePath, JSON.stringify(blacklist, null, 4), 'utf8');
await fs.promises.writeFile(blacklistFilePath, JSON.stringify(blacklist, null, 4), 'utf8');
return false;
}
return false;
} catch (err) {
console.error(`读取黑名单文件出错:${err.message}`);
sysLogger.error(`Error reading blacklist file: ${err.message}`);
return false;
}
});
}
// 将IP添加到黑名单
// Add IP to blacklist
async addToBlacklist(ip) {
await this.blacklistMutex.runExclusive(async () => {
try {
if (!fs.existsSync(blacklistFilePath)) {
fs.writeFileSync(blacklistFilePath, JSON.stringify({}), 'utf8');
try {
await fs.promises.access(blacklistFilePath, fs.constants.F_OK);
} catch (err) {
await fs.promises.writeFile(blacklistFilePath, JSON.stringify({}), 'utf8');
}
const dataRaw = fs.readFileSync(blacklistFilePath, 'utf8');
const dataRaw = await fs.promises.readFile(blacklistFilePath, 'utf8');
const blacklist = JSON.parse(dataRaw || '{}');
if (!blacklist[ip]) {
blacklist[ip] = {permanent: true};
fs.writeFileSync(
await fs.promises.writeFile(
blacklistFilePath,
JSON.stringify(blacklist, null, 4),
'utf8'
);
}
} catch (err) {
console.error(`写入黑名单文件出错:${err.message}`);
sysLogger.error(`Error writing blacklist file: ${err.message}`);
}
});
}
// 检查是否有临时限制
// Check for temp limits
_hasTemporaryLimit(ip) {
const now = Date.now();
if (this.temporaryLimits[ip] && now < this.temporaryLimits[ip]) {
@@ -203,7 +186,7 @@ class RequestLogger {
return false;
}
// 获取剩余限制时间
// Get remaining limit time
_getRemainingTime(ip) {
const now = Date.now();
if (this.temporaryLimits[ip] && now < this.temporaryLimits[ip]) {
@@ -212,45 +195,45 @@ class RequestLogger {
const mm = Math.floor((remaining % 3600000) / 60000);
const ss = Math.floor((remaining % 60000) / 1000);
if (hh > 0) {
return `${hh}小时${mm}${ss}`;
return `${hh}${mm}m ${ss}s`;
} else if (mm > 0) {
return `${mm}${ss}`;
return `${mm}m ${ss}s`;
} else {
return `${ss}`;
return `${ss}s`;
}
}
return '未知时间';
return 'Unknown time';
}
// 清理过期的请求记录
// Clean expired request records
_cleanUpRecords(ip) {
const now = Date.now();
// 请求记录保留24小时
// Request records kept for 24 hours
if (this.ipRequestRecords[ip]) {
this.ipRequestRecords[ip] = this.ipRequestRecords[ip].filter(
t => now - t <= 24 * 60 * 60 * 1000
);
}
// 30秒触发记录同样保留24小时
// 30-second trigger records also kept for 24 hours
if (this.ip30sTriggerCount[ip]) {
this.ip30sTriggerCount[ip] = this.ip30sTriggerCount[ip].filter(
t => now - t <= 24 * 60 * 60 * 1000
);
}
// 清理过期的临时限制
// Clean expired temp limits
if (this.temporaryLimits[ip] && now >= this.temporaryLimits[ip]) {
delete this.temporaryLimits[ip];
this.saveTempLimitsToFile();
}
}
// 定期清理长期未活动的IP数据
// Periodically clean inactive IP data
cleanUpInactiveIPs() {
for (const ip in this.ipRequestRecords) {
const noRecentRequests = !this.ipRequestRecords[ip] || this.ipRequestRecords[ip].length === 0;
const no30sTriggers = !this.ip30sTriggerCount[ip] || this.ip30sTriggerCount[ip].length === 0;
// 若该IP无请求记录、无30秒触发记录且无临时限制
// If this IP has no request record, no 30s trigger, and no temp limit
if (noRecentRequests && no30sTriggers && !this._hasTemporaryLimit(ip)) {
delete this.ipRequestRecords[ip];
delete this.ip30sTriggerCount[ip];
@@ -259,10 +242,10 @@ class RequestLogger {
}
}
// 记录请求并检测
// Record request and detect
async logRequest({time, ip, location, model, session}) {
if (ENABLE_DETECTION && await this.isBlacklisted(ip)) {
throw new Error(`您已被永久限制。`);
throw new Error(`You have been permanently limited.`);
}
const baseInfo = `IP: ${ip} | Location: ${location} | Model: ${model} | Session: ${session}`;
if (!ENABLE_DETECTION) {
@@ -278,31 +261,31 @@ class RequestLogger {
await ipMutex.runExclusive(() => {
const now = Date.now();
// 检查临时限制
// Check temp limits
if (this._hasTemporaryLimit(ip)) {
const rt = this._getRemainingTime(ip);
limitInfo = ` | Limit: ${this.temporaryLimits[ip] - now}ms`;
throw new Error(`您被限制访问,请在${rt}后再试。`);
throw new Error(`You are rate limited, please try again after ${rt}.`);
}
// 记录请求 & 清理
// Record request & cleanup
if (!this.ipRequestRecords[ip]) {
this.ipRequestRecords[ip] = [];
}
this.ipRequestRecords[ip].push(now);
this._cleanUpRecords(ip);
// >=4次 → 加入黑名单
// >=4 times -> Add to blacklist
const requestsIn6s = this.ipRequestRecords[ip].filter(
t => now - t <= 6 * 1000
);
if (requestsIn6s.length >= 3) {
this.addToBlacklist(ip);
limitInfo = ' | Limit: PERMANENT';
throw new Error(`并发请求过多,已永久限制。`);
throw new Error(`Too many concurrent requests, permanently limited.`);
}
// (>=3次 → 临时限制)
// (>=3 times -> Temporary limit)
const requestsIn30s = this.ipRequestRecords[ip].filter(
t => now - t <= 30 * 1000
);
@@ -313,19 +296,19 @@ class RequestLogger {
this.ip30sTriggerCount[ip].push(now);
this._cleanUpRecords(ip);
// 24小时内触发次数 >= 5 → 限制6小时
// Triggered >=5 times in 24h -> limit 6 hours
if (this.ip30sTriggerCount[ip].length >= 5) {
const duration = 6 * 60 * 60 * 1000;
this.temporaryLimits[ip] = now + duration;
limitInfo = ` | Limit: ${duration}ms`;
this.saveTempLimitsToFile();
throw new Error(`您在24小时内多次频繁请求,限制6小时`);
throw new Error(`Frequent requests in 24 hours, limited for 6 hours`);
} else {
const duration = 60 * 1000;
this.temporaryLimits[ip] = now + duration;
limitInfo = ` | Limit: ${duration}ms`;
this.saveTempLimitsToFile();
throw new Error(`请求过多, 限制1分钟`);
throw new Error(`Too many requests, limited for 1 minute`);
}
}
});
+93 -102
View File
@@ -7,26 +7,20 @@ import {fileURLToPath} from 'url';
import {optimizeBrowserDisplay} from './utils/browserDisplayFixer.mjs';
import {launchEdgeBrowser} from './utils/edgeLauncher.mjs';
import {setupBrowserFingerprint} from './utils/browserFingerprint.mjs';
import sysLogger from './utils/sysLogger.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const isHeadless = process.env.HEADLESS_BROWSER === 'true' && process.env.USE_MANUAL_LOGIN !== 'true';
let puppeteerModule;
let connect;
if (isHeadless === false) {
puppeteerModule = await import('puppeteer-real-browser');
connect = puppeteerModule.connect;
} else {
puppeteerModule = await import('puppeteer-core');
}
const puppeteerModule = await import('puppeteer-core');
// 会话自动释放时间(秒)
// Session auto-release timeout (seconds)
const SESSION_LOCK_TIMEOUT = parseInt(process.env.SESSION_LOCK_TIMEOUT || '0', 10);
// 存储已达请求上限的账号(格式: "timestamp | username")
const cooldownFilePath = path.join(__dirname, 'cooldownAccounts.log');
// Store accounts that reached request limit (format: "timestamp | username")
const cooldownFilePath = path.join(process.cwd(), 'data', 'cooldownAccounts.log');
// 冷却时长(默认24小时)
// Cooldown duration (default 24h)
const COOLDOWN_DURATION = 24 * 60 * 60 * 1000;
class SessionManager {
@@ -34,14 +28,14 @@ class SessionManager {
this.provider = provider;
this.isCustomModeEnabled = process.env.USE_CUSTOM_MODE === 'true';
this.isRotationEnabled = process.env.ENABLE_MODE_ROTATION === 'true';
this.isHeadless = isHeadless; // 是否隐藏浏览器
this.isHeadless = isHeadless; // Browser
this.currentIndex = 0;
this.usernameList = []; // 缓存用户名列表
this.browserInstances = []; // 浏览器实例数组
this.browserMutex = new Mutex(); // 浏览器互斥锁
this.usernameList = []; // Cache username list
this.browserInstances = []; // Browser
this.browserMutex = new Mutex(); // BrowserMutex lock
this.browserIndex = 0;
this.sessionAutoUnlockTimers = {}; // 自动解锁计时器
this.cooldownList = this.loadCooldownList(); // 加载并清理 cooldown 文件
this.sessionAutoUnlockTimers = {}; // Auto-unlock timer
this.cooldownList = this.loadCooldownList(); // Load and clean cooldown file
this.cleanupCooldownList();
}
@@ -49,13 +43,13 @@ class SessionManager {
this.sessions = sessions;
this.usernameList = Object.keys(this.sessions);
// 为每个 session 初始化相关属性
// Initialize relevant properties for each session
for (const username in this.sessions) {
const session = this.sessions[username];
session.locked = false; // 标记会话是否被锁定
session.requestCount = 0; // 请求计数
session.valid = true; // 标记会话是否有效
session.mutex = new Mutex(); // 创建互斥锁
session.locked = false; // Lock
session.requestCount = 0; // Request count
session.valid = true; // Mark session as valid
session.mutex = new Mutex(); // CreateMutex lock
if (session.currentMode === undefined) {
session.currentMode = this.isCustomModeEnabled ? 'custom' : 'default';
}
@@ -65,15 +59,15 @@ class SessionManager {
custom: true,
};
}
session.rotationEnabled = true; // 是否启用模式轮换
session.switchCounter = 0; // 模式切换计数器
session.requestsInCurrentMode = 0; // 当前模式下的请求次数
session.lastDefaultThreshold = 0; // 上次默认模式阈值
session.rotationEnabled = true; // Enable mode rotation
session.switchCounter = 0; // Mode switch counter
session.requestsInCurrentMode = 0; // Request count in current mode
session.lastDefaultThreshold = 0; // Last default mode threshold
session.switchThreshold = this.provider.getRandomSwitchThreshold(session);
// 记录请求次数
// Record request count
session.youTotalRequests = 0;
// 权重
// Weight
if (typeof session.weight !== 'number') {
session.weight = 1;
}
@@ -104,7 +98,7 @@ class SessionManager {
}
return arr;
} catch (err) {
console.error(`读取 ${cooldownFilePath} 出错:`, err);
sysLogger.error(`Error reading ${cooldownFilePath}:`, err);
return [];
}
}
@@ -112,13 +106,15 @@ class SessionManager {
saveCooldownList() {
try {
const lines = this.cooldownList.map(item => `${item.time} | ${item.username}`);
fs.writeFileSync(cooldownFilePath, lines.join('\n') + '\n', 'utf8');
fs.promises.writeFile(cooldownFilePath, lines.join('\n') + '\n', 'utf8').catch(err => {
sysLogger.error(`Error async writing ${cooldownFilePath}:`, err);
});
} catch (err) {
console.error(`写入 ${cooldownFilePath} 出错:`, err);
sysLogger.error(`Error preparing to write data to ${cooldownFilePath}:`, err);
}
}
// 清理过期(超过指定冷却时长)
// Clean expired (exceeded cooldown duration)
cleanupCooldownList() {
const now = Date.now();
let changed = false;
@@ -138,22 +134,22 @@ class SessionManager {
if (!already) {
this.cooldownList.push({time: now, username});
this.saveCooldownList();
console.log(`写入冷却列表:${new Date(now).toLocaleString()} | ${username}`);
sysLogger.debug(`Write to cooldown list: ${new Date(now).toLocaleString()} | ${username}`);
}
}
// 是否在冷却期(24小时内)
// In cooldown period (24 hours)?
isInCooldown(username) {
this.cleanupCooldownList();
return this.cooldownList.some(item => item.username === username);
}
// 批量初始化浏览器实例
// Browser
async initBrowserInstancesInBatch() {
const browserCount = parseInt(process.env.BROWSER_INSTANCE_COUNT) || 1;
// 可以是 'chrome', 'edge', 'auto'
// Can be 'chrome', 'edge', or 'auto'
const browserPath = detectBrowser(process.env.BROWSER_TYPE || 'auto');
const sharedProfilePath = path.join(__dirname, 'browser_profiles');
const sharedProfilePath = path.join(process.cwd(), 'browser_profiles');
createDirectoryIfNotExists(sharedProfilePath);
const tasks = [];
@@ -162,14 +158,31 @@ class SessionManager {
const userDataDir = path.join(sharedProfilePath, browserId);
createDirectoryIfNotExists(userDataDir);
// Clean up stale Chrome lock files caused by dirty container restarts
// Note: Chrome creates SingletonLock as a symlink, which fs.existsSync()
// cannot detect when the symlink target (the old PID) no longer exists.
// We use lstatSync instead which can see broken symlinks.
const lockFile = path.join(userDataDir, 'SingletonLock');
try {
await fs.promises.lstat(lockFile);
// If we get here, the file/symlink exists
await fs.promises.unlink(lockFile);
sysLogger.debug(`Deleted stale SingletonLock for ${browserId}`);
} catch (e) {
// ENOENT means file doesn't exist, which is fine
if (e.code !== 'ENOENT') {
sysLogger.warn(`Could not delete SingletonLock for ${browserId}:`, e);
}
}
tasks.push(this.launchSingleBrowser(browserId, userDataDir, browserPath));
}
// 并行执行
// Execute in parallel
const results = await Promise.all(tasks);
for (const instanceInfo of results) {
this.browserInstances.push(instanceInfo);
console.log(`创建浏览器实例: ${instanceInfo.id}`);
sysLogger.debug(`Creating browser instance: ${instanceInfo.id}`);
}
}
@@ -184,35 +197,14 @@ class SessionManager {
browser = result.browser;
page = result.page;
console.log(`Edge浏览器启动成功 (browserId=${browserId})`);
sysLogger.debug(`EdgeBrowser (browserId=${browserId})`);
} catch (error) {
console.error(`原生启动Edge失败:`, error);
console.log(`回退标准方式启动浏览器...`);
sysLogger.error(`EdgeFailed:`, error);
sysLogger.debug(`Browser...`);
}
}
if (!browser) {
if (isHeadless === false) {
// 使用 puppeteer-real-browser
const response = await connect({
headless: 'auto',
turnstile: true,
customConfig: {
userDataDir: userDataDir,
executablePath: browserPath,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--remote-debugging-address=::',
'--window-size=1280,850',
'--force-device-scale-factor=1',
],
},
});
browser = response.browser;
page = response.page;
} else {
// 使用 puppeteer-core
browser = await puppeteerModule.launch({
headless: this.isHeadless,
executablePath: browserPath,
@@ -229,10 +221,9 @@ class SessionManager {
});
page = await browser.newPage();
}
}
const originalUserAgent = await page.evaluate(() => navigator.userAgent);
// console.log(`浏览器 ${browserId} 原始用户代理: ${originalUserAgent}`);
// sysLogger.debug(`Browser ${browserId} original user agent: ${originalUserAgent}`);
const browserType = isEdge ? 'edge' : 'chrome';
const fingerprint = await setupBrowserFingerprint(page, browserType);
@@ -242,16 +233,16 @@ class SessionManager {
const newPlatform = await page.evaluate(() => navigator.platform);
const newCores = await page.evaluate(() => navigator.hardwareConcurrency);
// console.log(`浏览器 ${browserId} 应用指纹后:`);
// console.log(`- 用户代理: ${newUserAgent}`);
// console.log(`- 平台: ${newPlatform}`);
// console.log(`- CPU核心: ${newCores}`);
// console.log(`- 内存: ${fingerprint.ram}GB`);
// console.log(`- 设备名称: ${fingerprint.deviceName}`);
// sysLogger.debug(`Browser ${browserId} Apply fingerprint:`);
// sysLogger.debug(`- User Agent: ${newUserAgent}`);
// sysLogger.debug(`- Platform: ${newPlatform}`);
// sysLogger.debug(`- CPU Cores: ${newCores}`);
// sysLogger.debug(`- RAM: ${fingerprint.ram}GB`);
// sysLogger.debug(`- Device Name: ${fingerprint.deviceName}`);
const isActuallyEdge = newUserAgent.includes('Edg');
// 应用显示优化
// Apply display optimization
try {
await optimizeBrowserDisplay(page, {
width: 1280,
@@ -262,7 +253,7 @@ class SessionManager {
isHeadless: this.isHeadless
});
} catch (error) {
console.warn(`显示优化失败:`, error);
sysLogger.warn(`Display optimization failed:`, error);
}
return {
@@ -271,10 +262,10 @@ class SessionManager {
page: page,
locked: false,
isEdgeBrowser: isActuallyEdge,
fingerprint: fingerprint // 存储指纹信息
fingerprint: fingerprint // Store fingerprint info
};
} catch (error) {
console.error(`验证指纹时出错:`, error);
sysLogger.error(`Error verifying fingerprint:`, error);
try {
await optimizeBrowserDisplay(page, {
@@ -286,7 +277,7 @@ class SessionManager {
isHeadless: this.isHeadless
});
} catch (displayError) {
console.warn(`显示优化失败:`, displayError);
sysLogger.warn(`Display optimization failed:`, displayError);
}
const isActuallyEdge = originalUserAgent.includes('Edg');
@@ -314,7 +305,7 @@ class SessionManager {
return browserInstance;
}
}
throw new Error('当前负载已饱和,请稍后再试(以达到最大并发)');
throw new Error('Current load is saturated, please try again later (maximum concurrency reached)');
});
}
@@ -330,17 +321,17 @@ class SessionManager {
async getAvailableSessions() {
const allSessionsLocked = this.usernameList.every(username => this.sessions[username].locked);
if (allSessionsLocked) {
throw new Error('所有会话处于饱和状态,请稍后再试(无可用账号)');
throw new Error('All sessions saturated, please try again later (no available accounts)');
}
// 收集所有valid && !locked && (不在冷却期)
// Collect all valid && !locked && (not in cooldown)
let candidates = [];
for (const username of this.usernameList) {
const session = this.sessions[username];
// 如果没被锁 并且 session.valid
// If not locked and session.valid
if (session.valid && !session.locked) {
if (this.provider.enableRequestLimit && this.isInCooldown(username)) {
// console.log(`账号 ${username} 处于 24 小时冷却中,跳过`);
// sysLogger.debug(`Account ${username} is in 24-hour cooldown, skipping`);
continue;
}
candidates.push(username);
@@ -348,22 +339,22 @@ class SessionManager {
}
if (candidates.length === 0) {
throw new Error('没有可用的会话');
throw new Error('No available sessions');
}
// 随机洗牌
// Random shuffle
shuffleArray(candidates);
// 加权抽签
// Weighted draw
let weightSum = 0;
for (const uname of candidates) {
weightSum += this.sessions[uname].weight;
}
// 生成随机
// Generate random
const randValue = Math.floor(Math.random() * weightSum) + 1;
// 遍历并扣减
// Traverse and deduct
let cumulative = 0;
let selectedUsername = null;
for (const uname of candidates) {
@@ -380,22 +371,22 @@ class SessionManager {
const selectedSession = this.sessions[selectedUsername];
// 再尝试锁定账号
// Lock
const result = await selectedSession.mutex.runExclusive(async () => {
if (selectedSession.locked) {
return null;
}
// 判断是否可用
// Determine if available
if (selectedSession.modeStatus && selectedSession.modeStatus[selectedSession.currentMode]) {
// 锁定
// Lock
selectedSession.locked = true;
selectedSession.requestCount++;
// 获取可用浏览器
// Browser
const browserInstance = await this.getAvailableBrowser();
// 启动自动解锁计时器
// Auto-unlock timer
if (SESSION_LOCK_TIMEOUT > 0) {
this.startAutoUnlockTimer(selectedUsername, browserInstance.id);
}
@@ -411,7 +402,7 @@ class SessionManager {
this.provider &&
typeof this.provider.switchMode === 'function'
) {
console.warn(`尝试为账号 ${selectedUsername} 切换模式...`);
sysLogger.warn(`Attempting to switch mode for account ${selectedUsername}...`);
this.provider.switchMode(selectedSession);
selectedSession.rotationEnabled = false;
@@ -438,12 +429,12 @@ class SessionManager {
if (result) {
return result;
} else {
throw new Error('会话刚被占用或模式不可用!');
throw new Error('Session just occupied or mode unavailable!');
}
}
startAutoUnlockTimer(username, browserId) {
// 清除可能残留计时器
// Clear potential residual timers
if (this.sessionAutoUnlockTimers[username]) {
clearTimeout(this.sessionAutoUnlockTimers[username]);
}
@@ -452,8 +443,8 @@ class SessionManager {
this.sessionAutoUnlockTimers[username] = setTimeout(async () => {
const session = this.sessions[username];
if (session && session.locked) {
console.warn(
`会话 "${username}" 已自动解锁`
sysLogger.warn(
`Session "${username}" has been automatically unlocked`
);
await session.mutex.runExclusive(async () => {
@@ -471,7 +462,7 @@ class SessionManager {
session.locked = false;
});
}
// 存在相应计时器清除
// Clear corresponding timer if exists
if (this.sessionAutoUnlockTimers[username]) {
clearTimeout(this.sessionAutoUnlockTimers[username]);
delete this.sessionAutoUnlockTimers[username];
@@ -482,22 +473,22 @@ class SessionManager {
}
}
// 返回会话
// Return session
// getBrowserInstances() {
// return this.browserInstances;
// }
// 策略
// Strategy
async getSessionByStrategy(strategy = 'round_robin') {
if (strategy === 'round_robin') {
return await this.getAvailableSessions();
}
throw new Error(`未实现的策略: ${strategy}`);
throw new Error(`Strategy: ${strategy}`);
}
}
/**
* FisherYates 洗牌
* FisherYates
*/
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
@@ -2,6 +2,7 @@ import os from 'os';
import fs from 'fs';
import path from 'path';
import {execSync} from 'child_process';
import sysLogger from './sysLogger.mjs';
export function detectBrowser(preferredBrowser = 'auto') {
const platform = os.platform();
@@ -19,7 +20,7 @@ export function detectBrowser(preferredBrowser = 'auto') {
} else if (platform === 'linux') {
browsers.chrome = findLinuxBrowser('google-chrome');
//Arch下AUR安装的chrome为google-chrome-stable
//Chrome installed via AUR on Arch is google-chrome-stable
if (browsers.chrome == null) {
browsers.chrome = findLinuxBrowser('google-chrome-stable');
}
@@ -34,11 +35,11 @@ export function detectBrowser(preferredBrowser = 'auto') {
return browsers.edge;
}
} else if (browsers[preferredBrowser]) {
console.log(`使用${preferredBrowser === 'chrome' ? 'Chrome' : 'Edge'}浏览器`);
sysLogger.debug(`${preferredBrowser === 'chrome' ? 'Chrome' : 'Edge'}Browser`);
return browsers[preferredBrowser];
}
console.error('未找到ChromeEdge浏览器,请确保已安装其中之一');
sysLogger.error('ChromeEdgeBrowser');
process.exit(1);
}
@@ -1,17 +1,18 @@
import sysLogger from './sysLogger.mjs';
/**
* @param {Object} page - Puppeteer
* @param {Object} options - 配置选项
* @param {number} options.width - 视口宽度
* @param {number} options.height - 视口高度
* @param {number} options.deviceScaleFactor - 设备缩放因子
* @param {boolean} options.isMobile - 模拟移动设备
* @param {boolean} options.hasTouch - 支持触摸
* @param {boolean} options.isLandscape - 横屏
* @param {Object} options -
* @param {number} options.width -
* @param {number} options.height -
* @param {number} options.deviceScaleFactor -
* @param {boolean} options.isMobile -
* @param {boolean} options.hasTouch -
* @param {boolean} options.isLandscape -
* @returns {Promise<void>}
*/
export async function fixBrowserDisplay(page, options = {}) {
if (!page) {
console.error('页面对象为空,无法修复显示');
sysLogger.error('Page object is empty, unable to fix display');
return;
}
@@ -27,7 +28,7 @@ export async function fixBrowserDisplay(page, options = {}) {
const settings = {...defaultOptions, ...options};
try {
// 设置视口大小和设备比例
// Set viewport size and device ratio
await page.setViewport({
width: settings.width,
height: settings.height,
@@ -37,7 +38,7 @@ export async function fixBrowserDisplay(page, options = {}) {
isLandscape: settings.isLandscape
});
// 尝试调整窗口大小
// Try resizing window
const session = await page.target().createCDPSession();
await session.send('Emulation.setDeviceMetricsOverride', {
width: settings.width,
@@ -48,13 +49,13 @@ export async function fixBrowserDisplay(page, options = {}) {
screenHeight: settings.height
});
// 重置页面缩放
// Reset page zoom
await page.evaluate(() => {
document.body.style.zoom = '100%';
document.body.style.transform = 'scale(1)';
document.body.style.transformOrigin = '0 0';
// 尝试修复可能存在的CSS
// Attempt to fix possible CSS
const styleElement = document.createElement('style');
styleElement.textContent = `
html, body {
@@ -74,14 +75,14 @@ export async function fixBrowserDisplay(page, options = {}) {
});
} catch (error) {
console.error('修复浏览器显示时出错:', error);
sysLogger.error('Browser:', error);
}
}
/**
* 调整CSS比例
* CSS
* @param {Object} page - Puppeteer
* @param {number} scale - 缩放比例
* @param {number} scale -
* @returns {Promise<void>}
*/
export async function adjustCssScaling(page, scale = 1) {
@@ -101,16 +102,16 @@ export async function adjustCssScaling(page, scale = 1) {
`;
document.head.appendChild(styleElem);
// 重新计算布局
// Recalculate layout
window.dispatchEvent(new Event('resize'));
}, scale);
} catch (error) {
console.error('调整CSS比例时出错:', error);
sysLogger.error('Error adjusting CSS ratio:', error);
}
}
/**
* 修复高DPI
* DPI
* @param {Object} page - Puppeteer
* @returns {Promise<void>}
*/
@@ -118,7 +119,7 @@ export async function fixHighDpiDisplay(page) {
if (!page) return;
try {
// 检测设备像素比
// Detect device pixel ratio
const devicePixelRatio = await page.evaluate(() => window.devicePixelRatio);
if (devicePixelRatio > 1) {
@@ -136,14 +137,14 @@ export async function fixHighDpiDisplay(page) {
}, devicePixelRatio);
}
} catch (error) {
console.error('修复高DPI显示时出错:', error);
sysLogger.error('Fix High-DPI display:', error);
}
}
/**
* 完整浏览器显示优化
* Browser
* @param {Object} page - Puppeteer
* @param {Object} options - 配置
* @param {Object} options -
* @returns {Promise<void>}
*/
export async function optimizeBrowserDisplay(page, options = {}) {
@@ -159,14 +160,14 @@ export async function optimizeBrowserDisplay(page, options = {}) {
const config = {...defaultOptions, ...options};
try {
// 基本显示修复
// Basic display fix
await fixBrowserDisplay(page, {
width: config.width,
height: config.height,
deviceScaleFactor: config.deviceScaleFactor
});
// 修复高DPI显示
// Fix High-DPI display
if (config.fixHighDpi) {
await fixHighDpiDisplay(page);
}
@@ -175,7 +176,7 @@ export async function optimizeBrowserDisplay(page, options = {}) {
await adjustCssScaling(page, config.cssScale);
}
// 如果强制调整窗口大小
// If forced to resize window
if (config.forceResize && !config.isHeadless) {
try {
const client = await page.target().createCDPSession();
@@ -188,19 +189,19 @@ export async function optimizeBrowserDisplay(page, options = {}) {
}
});
} catch (resizeError) {
// console.log('无法调整窗口大小:', resizeError.message);
// sysLogger.debug('Unable to resize window:', resizeError.message);
try {
await page.evaluate((width, height) => {
window.resizeTo(width, height);
}, config.width, config.height);
} catch (altError) {
console.log('失败:', altError.message);
sysLogger.debug('Failed:', altError.message);
}
}
}
} catch (error) {
console.error('浏览器显示优化失败:', error);
sysLogger.error('BrowserDisplay optimization failed:', error);
}
}
@@ -1,11 +1,12 @@
import path from 'path';
import {fileURLToPath} from 'url';
import crypto from 'crypto';
import sysLogger from './sysLogger.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 操作系统
// System
const osList = [
{
name: 'Windows',
@@ -34,11 +35,11 @@ const osList = [
}
];
// 浏览器
// Browser
const browserVersions = {
'chrome': {
name: 'Chrome',
// 主版本.次版本.构建号.补丁号
// Major.Minor.Build.Patch
majorVersions: [120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134],
minorVersions: [0, 1, 2, 3],
buildVersions: [5000, 5500, 6000, 6500, 6700, 6800, 6900, 7000, 7100],
@@ -89,7 +90,7 @@ const browserVersions = {
}
};
// WebGL 供应商和渲染器映射表
// WebGL vendor and renderer map
const gpuInfo = {
'NVIDIA': [
'ANGLE (NVIDIA, NVIDIA GeForce GTX 1650 Direct3D11 vs_5_0 ps_5_0, D3D11)',
@@ -151,21 +152,21 @@ const gpuInfo = {
]
};
// 设备名称列表
// Device name list
const deviceNames = [
// Windows
'DESKTOP-', 'LAPTOP-', 'PC-', 'WIN-', 'WORKSTATION-',
// Mac
'MacBook-Pro', 'MacBook-Air', 'iMac-Pro', 'Mac-mini', 'Mac-Studio',
// 通用
// General
'DELL-', 'HP-', 'LENOVO-', 'ASUS-', 'ACER-', 'MSI-', 'ALIENWARE-', 'GIGABYTE-'
];
// 生成随机设备名称
// Generate randomDevice Name
function generateDeviceNameSuffix() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
const length = Math.floor(Math.random() * 6) + 4; // 4-9
const length = Math.floor(Math.random() * 6) + 4; // 4-9 digits
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
@@ -174,7 +175,7 @@ function generateDeviceNameSuffix() {
return result;
}
// MAC地址
// MAC Address
const macPrefixes = [
'E8-2A-EA', '00-1A-2B', 'AC-DE-48', 'B8-27-EB', 'DC-A6-32',
'00-50-56', '00-0C-29', '00-05-69', '00-25-90', 'BC-5F-F4',
@@ -182,7 +183,7 @@ const macPrefixes = [
'B4-2E-99', '8C-85-90', '34-97-F6', 'A4-83-E7', '78-7B-8A'
];
// 区域语言
// Regional language
const localeSettings = {
'en-US': {
languages: ['en-US', 'en'],
@@ -246,7 +247,7 @@ const localeSettings = {
}
};
// CPU核心
// CPU Cores
const computerSpecs = [
{cores: 2, ram: [2, 4]},
{cores: 4, ram: [4, 8, 16]},
@@ -259,7 +260,7 @@ const computerSpecs = [
{cores: 32, ram: [64, 128, 256]}
];
// 插件
// Plugin
const browserPlugins = {
'chrome': [
{
@@ -321,7 +322,7 @@ const browserPlugins = {
};
/**
* 随机整数
*
* @param {number} min
* @param {number} max
* @returns {number}
@@ -331,8 +332,8 @@ function getRandomInt(min, max) {
}
/**
* @param {Array} array - 选项数组
* @returns {*} - 随机选择的项
* @param {Array} array -
* @returns {*} -
*/
function randomChoice(array) {
if (!Array.isArray(array) || array.length === 0) {
@@ -342,25 +343,25 @@ function randomChoice(array) {
}
/**
* @param {number} percentChance - true概率百分比 (0-100)
* @returns {boolean} - 随机布尔值
* @param {number} percentChance - true (0-100)
* @returns {boolean} -
*/
function randomChance(percentChance) {
return Math.random() * 100 < percentChance;
}
/**
* @returns {string} - 随机种子
* @returns {string} -
*/
function createRandomSeed() {
return crypto.randomBytes(16).toString('hex');
}
/**
* @param {string} seed - 种子字符串
* @param {number} min - 最小值
* @param {number} max - 最大值
* @returns {number} - 伪随机数
* @param {string} seed -
* @param {number} min -
* @param {number} max -
* @returns {number} -
*/
function seededRandom(seed, min, max) {
const hash = crypto.createHash('sha256').update(seed).digest('hex');
@@ -369,7 +370,7 @@ function seededRandom(seed, min, max) {
}
/**
* 随机MAC地址
* MAC Address
* @returns {string}
*/
function generateRandomMAC() {
@@ -382,37 +383,37 @@ function generateRandomMAC() {
}
/**
* 详细版本号
* @param {Object} browser - 浏览器
* @returns {string} - "133.0.6834.110"
* Version number
* @param {Object} browser - Browser
* @returns {string} - "133.0.6834.110"
*/
function generateRealisticVersion(browser) {
if (!browser || !browser.majorVersions) {
return "100.0.0.0"; // 默认值
return "100.0.0.0"; // Default value
}
const majorVersion = randomChoice(browser.majorVersions);
const minorVersion = randomChoice(browser.minorVersions || [0]);
// Chrome/Edge 风格: 133.0.6834.110
// Chrome/Edge style: 133.0.6834.110
if (browser.buildVersions && browser.buildVersions.length > 0 && browser.patchVersions && browser.patchVersions.length > 0) {
const buildVersion = randomChoice(browser.buildVersions);
const patchVersion = randomChoice(browser.patchVersions);
return `${majorVersion}.${minorVersion}.${buildVersion}.${patchVersion}`;
}
// Firefox 风格: 123.0.1
// Firefox style: 123.0.1
else if (browser.minorVersions && browser.minorVersions.length > 0) {
return `${majorVersion}.${minorVersion}`;
}
// 简单: 15.4
// Simple: 15.4
else {
return `${majorVersion}.${getRandomInt(0, 9)}`;
}
}
/**
* 随机用户代理
* @param {string} browserType - 浏览器类型
*
* @param {string} browserType - Browser
* @returns {string}
*/
function generateRealisticUserAgent(browserType = null) {
@@ -424,16 +425,16 @@ function generateRealisticUserAgent(browserType = null) {
browser = browserVersions[randomChoice(browserKeys)];
}
// 系统
// System
const os = randomChoice(osList);
const osVersion = randomChoice(os.versions);
const platform = randomChoice(os.platforms);
// 版本号
// Version number
const version = generateRealisticVersion(browser);
browser.fullVersion = version;
// 分离版本号
// Separate version number
const majorVersionPart = version.split('.')[0];
let userAgent;
@@ -489,7 +490,7 @@ function generateRealisticUserAgent(browserType = null) {
}
}
// 如果没有匹配到任何合适的组合,提供一个默认UA
// If no suitable combination matched, provide a default UA
if (!userAgent) {
userAgent = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36`;
}
@@ -498,9 +499,9 @@ function generateRealisticUserAgent(browserType = null) {
}
/**
* 随机字符串
* @param {number} length - 长度
* @param {boolean} upperOnly - 是否仅大写字母
*
* @param {number} length -
* @param {boolean} upperOnly -
* @returns {string}
*/
function getRandomString(length, upperOnly = false) {
@@ -515,9 +516,9 @@ function getRandomString(length, upperOnly = false) {
}
/**
* 一致浏览器指纹
* @param {Object} options - 配置选项
* @returns {Object} - 指纹数据
* Browser
* @param {Object} options -
* @returns {Object} -
*/
export function generateFingerprint(options = {}) {
const seed = options.seed || createRandomSeed();
@@ -526,7 +527,7 @@ export function generateFingerprint(options = {}) {
if (browserType && typeof browserType === 'string') {
browserType = browserType.toLowerCase();
if (!browserVersions[browserType]) {
console.warn(`未支持的浏览器类型: ${browserType}将使用随机浏览器类型`);
sysLogger.warn(`Browser: ${browserType}Browser`);
browserType = null;
}
}
@@ -536,21 +537,21 @@ export function generateFingerprint(options = {}) {
browserType = browserKeys[Math.floor(Math.random() * browserKeys.length)];
}
// 生成用户代理
// Generate user agent
const userAgent = options.userAgent || generateRealisticUserAgent(browserType);
// 选择地区/语言
// /Language
const locale = options.locale || randomChoice(Object.keys(localeSettings));
const localeData = localeSettings[locale];
// 计算机规格
// Computer specs
const computerSpec = options.computerSpec || randomChoice(computerSpecs);
// 选择GPU信息
// Select GPU info
const gpuVendor = options.gpuVendor || randomChoice(Object.keys(gpuInfo));
const gpuRenderer = options.gpuRenderer || randomChoice(gpuInfo[gpuVendor] || ['']);
// 设备名称
// Device Name
const deviceNameBase = options.deviceNameBase || randomChoice(deviceNames);
const deviceName = options.deviceName ||
(deviceNameBase.endsWith('-') ?
@@ -570,14 +571,14 @@ export function generateFingerprint(options = {}) {
} else if (userAgent.includes('iPhone') || userAgent.includes('iPad')) {
platform = userAgent.includes('iPad') ? 'iPad' : 'iPhone';
} else {
platform = 'Win32'; // 默认
platform = 'Win32'; // Default
}
}
// 插件列表
// Plugin list
const plugins = options.plugins || (browserPlugins[browserType] || []);
// 创建
// Create
return {
seed,
userAgent,
@@ -589,17 +590,17 @@ export function generateFingerprint(options = {}) {
timezone: options.timezone || randomChoice(localeData.timeZones || ['UTC']),
geolocation: options.geolocation || 'prompt',
// 语言和区域设置
// Language
language: options.language || locale,
languages: options.languages || localeData.languages || [locale, 'en-US'],
// 指纹保护
// Fingerprint protection
canvas: options.canvas || 'noise',
webGL: options.webGL || 'noise',
audioContext: options.audioContext || 'noise',
mediaDevices: options.mediaDevices || 'noise',
// 硬件信息
// Hardware info
webGLMetadata: {
vendor: `Google Inc. (${gpuVendor})`,
renderer: gpuRenderer,
@@ -607,7 +608,7 @@ export function generateFingerprint(options = {}) {
rendererUnmasked: gpuRenderer
},
// 系统资源
// System
cpu: {
cores: Number(options.cpuCores || computerSpec.cores),
architecture: options.cpuArchitecture || 'x86-64'
@@ -616,16 +617,16 @@ export function generateFingerprint(options = {}) {
deviceName: deviceName,
macAddress: options.macAddress || generateRandomMAC(),
// 其他设置
// Other settings
doNotTrack: options.doNotTrack !== undefined ? options.doNotTrack : randomChoice([null, '0', '1']),
hardwareAcceleration: options.hardwareAcceleration || 'default',
plugins: plugins,
screenOrientation: options.screenOrientation || 'landscape-primary',
// 版本信息
// Version info
browserVersion: getBrowserVersionFromUA(userAgent),
// 指纹强度
// Fingerprint strength
noiseLevel: options.noiseLevel || 'medium', // low, medium, high
consistencyLevel: options.consistencyLevel || 'high', // low, medium, high
@@ -638,9 +639,9 @@ export function generateFingerprint(options = {}) {
}
/**
* 从用户代理确定操作系统
* @param {string} userAgent - 用户代理
* @returns {Object} - 操作系统信息
* System
* @param {string} userAgent -
* @returns {Object} - System
*/
function determineOsInfo(userAgent) {
let name, version, archType;
@@ -652,7 +653,7 @@ function determineOsInfo(userAgent) {
} else if (userAgent.includes('Windows NT 11.0')) {
version = '11';
} else {
version = '10'; // 默认Windows 10
version = '10'; // Default Windows 10
}
archType = userAgent.includes('Win64') || userAgent.includes('x64') ? 'x64' : 'x86';
} else if (userAgent.includes('Mac OS X') || userAgent.includes('Macintosh')) {
@@ -685,9 +686,9 @@ function determineOsInfo(userAgent) {
}
/**
* 从用户代理提取浏览器版本
* @param {string} userAgent - 用户代理
* @returns {Object} - 浏览器版本
* Browser
* @param {string} userAgent -
* @returns {Object} - Browser
*/
function getBrowserVersionFromUA(userAgent) {
let name, version, fullVersion;
@@ -717,23 +718,23 @@ function getBrowserVersionFromUA(userAgent) {
fullVersion = '1.0.0';
}
version = fullVersion.split('.')[0]; // 主版本号
version = fullVersion.split('.')[0]; // Major version
return {name, version, fullVersion};
}
/**
* 指纹应用到浏览器页面
* @param {Object} page - Puppeteer页面对象
* @param {Object} fingerprint - 指纹对象
* Browser
* @param {Object} page - Puppeteer
* @param {Object} fingerprint -
* @returns {Promise<boolean>}
*/
export async function applyFingerprint(page, fingerprint) {
try {
// 设置用户代理
// Set user agent
await page.setUserAgent(fingerprint.userAgent);
// 设置语言和时区
// Set language and timezone
await page.setExtraHTTPHeaders({
'Accept-Language': fingerprint.languages.join(',')
});
@@ -769,19 +770,19 @@ export async function applyFingerprint(page, fingerprint) {
}
};
// 保存原始navigator
// Save original navigator
const originalNavigator = window.navigator;
const properties = Object.getOwnPropertyDescriptors(window.navigator);
const resultNavigator = {};
// 允许的可写属性
// Allowed writable properties
const allowedToWrite = [
'userAgent', 'appVersion', 'platform', 'language', 'languages',
'deviceMemory', 'hardwareConcurrency', 'doNotTrack', 'webdriver',
'maxTouchPoints'
];
// 适当UA模拟
// Appropriate UA simulation
const browserInfo = fp.browserVersion;
for (const key in properties) {
@@ -829,7 +830,7 @@ export async function applyFingerprint(page, fingerprint) {
break;
}
// 如果有覆盖值
// If there is an override value
if (overrideValue !== undefined) {
Object.defineProperty(resultNavigator, key, {
value: overrideValue,
@@ -838,7 +839,7 @@ export async function applyFingerprint(page, fingerprint) {
writable: false
});
} else if (properties[key].configurable) {
// 从原始 navigator 获取
// Get from original navigator
Object.defineProperty(resultNavigator, key, {
get: function () {
try {
@@ -851,7 +852,7 @@ export async function applyFingerprint(page, fingerprint) {
configurable: false
});
} else {
// 不可配置的属性,保持原样
// Unconfigurable property, kept as is
if (properties[key].writable) {
resultNavigator[key] = originalNavigator[key];
} else {
@@ -865,14 +866,14 @@ export async function applyFingerprint(page, fingerprint) {
}
}
// 创建代理以拦截任何新添加的属性
// Create
const navigatorProxy = new Proxy(resultNavigator, {
has: (target, key) => key in target || key in originalNavigator,
get: (target, key) => {
if (key in target) {
return target[key];
}
// 使用原始值
// Use original value
return originalNavigator[key];
},
set: (target, key, value) => {
@@ -895,7 +896,7 @@ export async function applyFingerprint(page, fingerprint) {
}
});
// 替换 navigator
// Replace navigator
Object.defineProperty(window, 'navigator', {
value: navigatorProxy,
writable: false,
@@ -904,7 +905,7 @@ export async function applyFingerprint(page, fingerprint) {
});
if ('userAgentData' in originalNavigator) {
// 创建伪造userAgentData
// CreateuserAgentData
const brandsList = [];
if (fp.browserType === 'chrome') {
@@ -980,7 +981,7 @@ export async function applyFingerprint(page, fingerprint) {
}
};
// 添加到 navigator
// Add to navigator
Object.defineProperty(navigatorProxy, 'userAgentData', {
value: uaData,
writable: false,
@@ -990,7 +991,7 @@ export async function applyFingerprint(page, fingerprint) {
}
if (fp.webRTC === false) {
// 阻止 WebRTC 泄露
// Prevent WebRTC leak
const origRTCPeerConnection = window.RTCPeerConnection ||
window.webkitRTCPeerConnection ||
window.mozRTCPeerConnection;
@@ -998,7 +999,7 @@ export async function applyFingerprint(page, fingerprint) {
if (origRTCPeerConnection) {
class CustomRTCPeerConnection extends origRTCPeerConnection {
constructor(configuration) {
// 过滤掉ICE服务器
// Filter out ICE servers
if (configuration && configuration.iceServers) {
configuration = {
...configuration,
@@ -1009,7 +1010,7 @@ export async function applyFingerprint(page, fingerprint) {
}
createOffer(...args) {
// 拦截createOffer
// Intercept createOffer
return new Promise((resolve, reject) => {
super.createOffer(...args)
.then(offer => {
@@ -1041,7 +1042,7 @@ export async function applyFingerprint(page, fingerprint) {
window.mozRTCPeerConnection = CustomRTCPeerConnection;
}
// 禁用媒体设备
// Disable media devices
const safeMediaDevices = {
enumerateDevices: function () {
return Promise.resolve([]);
@@ -1079,13 +1080,13 @@ export async function applyFingerprint(page, fingerprint) {
if (!context) return null;
if (type === '2d') {
// 2D Canvas指纹保护
// 2D Canvas fingerprint protection
const origGetImageData = context.getImageData;
const origPutImageData = context.putImageData;
const origToDataURL = this.toDataURL;
const origToBlob = this.toBlob;
// 添加微小噪声的函数
// Noise
const addNoise = function (data) {
const noise = Math.floor(Math.random() * 10) / 255;
for (let i = 0; i < data.data.length; i += 4) {
@@ -1109,7 +1110,7 @@ export async function applyFingerprint(page, fingerprint) {
const dataURL = origToDataURL.apply(this, args);
if (!dataURL) return dataURL;
// URL添加微小噪声 (改变最后几个字符)
// URLNoise ()
const lastCommaIndex = dataURL.lastIndexOf(',');
if (lastCommaIndex !== -1) {
const prefix = dataURL.substring(0, lastCommaIndex + 1);
@@ -1132,7 +1133,7 @@ export async function applyFingerprint(page, fingerprint) {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
// 修改dataURL
// Modify dataURL
const dataURL = reader.result;
const lastCommaIndex = dataURL.lastIndexOf(',');
if (lastCommaIndex !== -1) {
@@ -1161,7 +1162,7 @@ export async function applyFingerprint(page, fingerprint) {
}, ...args);
};
} else if (type.includes('webgl') || type.includes('experimental-webgl')) {
// WebGL Canvas指纹保护
// WebGL Canvas fingerprint protection
const origGetParameter = context.getParameter;
context.getParameter = function (parameter) {
@@ -1188,7 +1189,7 @@ export async function applyFingerprint(page, fingerprint) {
if (AudioContext) {
const origAudioContext = AudioContext;
// 创建带噪声AudioContext
// CreateNoiseAudioContext
window.AudioContext = window.webkitAudioContext = function () {
const ctx = new origAudioContext();
@@ -1196,7 +1197,7 @@ export async function applyFingerprint(page, fingerprint) {
if (origGetChannelData) {
ctx.createAnalyser().getFloatFrequencyData = function (array) {
origGetChannelData.call(this, array);
// 添加微小噪声
// Noise
for (let i = 0; i < array.length; i += 50) {
if (array[i]) {
array[i] += (Math.random() * 0.0001) - 0.00005;
@@ -1210,7 +1211,7 @@ export async function applyFingerprint(page, fingerprint) {
}
}
// 创建自定义插件列表
// Create custom plugin list
const mimeTypeArray = [];
const pluginArray = [];
@@ -1218,7 +1219,7 @@ export async function applyFingerprint(page, fingerprint) {
fp.plugins.forEach((plugin, pluginIndex) => {
if (!plugin || !plugin.name) return;
// 创建MimeTypes
// CreateMimeTypes
const mimeTypes = {};
let mimeTypeCount = 0;
@@ -1239,7 +1240,7 @@ export async function applyFingerprint(page, fingerprint) {
});
}
// 创建插件对象
// Create plugin object
const pluginObj = {
name: plugin.name,
filename: plugin.filename || '',
@@ -1253,12 +1254,12 @@ export async function applyFingerprint(page, fingerprint) {
}
};
// 扩展插件对象
// Extend plugin object
for (let i = 0; i < mimeTypeCount; i++) {
pluginObj[i] = mimeTypes[i];
}
// mime设置enabledPlugin
// mime set enabledPlugin
Object.values(mimeTypes).forEach(mime => {
mime.enabledPlugin = pluginObj;
});
@@ -1266,7 +1267,7 @@ export async function applyFingerprint(page, fingerprint) {
pluginArray.push(pluginObj);
});
// 自定义的navigator.plugins
// Custom navigator.plugins
const pluginsObj = {
length: pluginArray.length,
item: function (index) {
@@ -1285,7 +1286,7 @@ export async function applyFingerprint(page, fingerprint) {
pluginsObj[plugin.name] = plugin;
}
// 自定义navigator.mimeTypes
// Custom navigator.mimeTypes
const mimeTypesObj = {
length: mimeTypeArray.length,
item: function (index) {
@@ -1302,7 +1303,7 @@ export async function applyFingerprint(page, fingerprint) {
mimeTypesObj[mimeType.type] = mimeType;
}
// pluginsmimeTypes附加到navigator
// Append plugins and mimeTypes to navigator
Object.defineProperty(navigatorProxy, 'plugins', {
value: pluginsObj,
writable: false,
@@ -1318,23 +1319,23 @@ export async function applyFingerprint(page, fingerprint) {
});
}
// 隐藏自动化
// Hide automation
Object.defineProperty(navigatorProxy, 'webdriver', {
get: () => false,
enumerable: true,
configurable: false
});
// 修复Chrome特征
// Fix Chrome characteristics
if (window.chrome) {
const chromeObj = {};
const originalChrome = window.chrome;
// 复制原始chrome
// Copy original chrome
for (const key in originalChrome) {
try {
if (key === 'runtime' && originalChrome.runtime) {
// 处理chrome.runtime
// Process chrome.runtime
const runtimeObj = {};
for (const rKey in originalChrome.runtime) {
try {
@@ -1350,7 +1351,7 @@ export async function applyFingerprint(page, fingerprint) {
}
}
// 创建chrome.app
// Create chrome.app
chromeObj.app = {
isInstalled: false,
InstallState: {
@@ -1365,7 +1366,7 @@ export async function applyFingerprint(page, fingerprint) {
}
};
// 替换chrome
// Replace chrome
Object.defineProperty(window, 'chrome', {
value: chromeObj,
writable: false,
@@ -1374,7 +1375,7 @@ export async function applyFingerprint(page, fingerprint) {
});
}
// 添加PDF查看器
// Add PDF viewer
if (fp.pdfViewerEnabled) {
for (const mimeType of ['application/pdf', 'text/pdf']) {
const pdfMime = {
@@ -1394,7 +1395,7 @@ export async function applyFingerprint(page, fingerprint) {
}
}
// 噪声
// Noise
if (fp.noiseLevel && (fp.noiseLevel === 'medium' || fp.noiseLevel === 'high')) {
const addNoise = (value, scale) => {
if (typeof value !== 'number') return value;
@@ -1403,7 +1404,7 @@ export async function applyFingerprint(page, fingerprint) {
};
}
// 清理webdriver
// Clean webdriver
delete window.__nightmare;
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
@@ -1424,7 +1425,7 @@ export async function applyFingerprint(page, fingerprint) {
return originalToString.call(this);
};
// 创建隐藏的指纹验证
// Create hidden fingerprint verification
window._fingerprintId = fp.seed;
const event = new CustomEvent('fingerprintApplied', {
@@ -1443,20 +1444,20 @@ export async function applyFingerprint(page, fingerprint) {
};
}, fingerprint);
// console.log('指纹应用调试信息:', debugInfo);
console.log(`已应用自定义浏览器指纹: ${fingerprint.userAgent}`);
// sysLogger.debug('Fingerprint debug info:', debugInfo);
sysLogger.info(`Applied custom browser fingerprint: ${fingerprint.userAgent}`);
return true;
} catch (error) {
console.error('应用浏览器指纹时出错:', error);
sysLogger.error('Browser:', error);
return false;
}
}
/**
* 为特定浏览器创建并应用指纹
* BrowserCreateApply fingerprint
* @param {Object} page - Puppeteer
* @param {string|Object} options - 浏览器或完整配置
* @returns {Promise<Object>} - 应用指纹
* @param {string|Object} options - Browser
* @returns {Promise<Object>} - Apply fingerprint
*/
export async function setupBrowserFingerprint(page, options = {}) {
try {
@@ -1464,27 +1465,27 @@ export async function setupBrowserFingerprint(page, options = {}) {
options = {browserType: options};
}
// 生成完整的指纹
// Generate complete fingerprint
const fingerprint = generateFingerprint(options);
// 应用指纹
// Apply fingerprint
const success = await applyFingerprint(page, fingerprint);
// 验证指纹应用
// Verify fingerprint application
if (success) {
try {
const appliedUserAgent = await page.evaluate(() => navigator.userAgent);
if (appliedUserAgent !== fingerprint.userAgent) {
console.warn('用户代理未正确应用:', {
sysLogger.warn('User agent not applied correctly:', {
expected: fingerprint.userAgent,
applied: appliedUserAgent
});
}
// 验证CPU核心数
// Verify CPU cores
// const hardwareConcurrency = await page.evaluate(() => navigator.hardwareConcurrency);
// if (Number(hardwareConcurrency) !== Number(fingerprint.cpu.cores)) {
// console.warn('CPU核心数未正确应用:', {
// sysLogger.warn('CPU cores not correctly applied:', {
// expected: fingerprint.cpu.cores,
// applied: hardwareConcurrency,
// expectedType: typeof fingerprint.cpu.cores,
@@ -1492,7 +1493,7 @@ export async function setupBrowserFingerprint(page, options = {}) {
// });
// }
// 验证WebGL
// Verify WebGL
if (fingerprint.webGL !== 'block') {
const webglVendor = await page.evaluate(() => {
try {
@@ -1505,27 +1506,27 @@ export async function setupBrowserFingerprint(page, options = {}) {
});
if (webglVendor && !webglVendor.includes(fingerprint.webGLMetadata.vendorUnmasked)) {
console.warn('WebGL供应商信息未正确应用');
sysLogger.warn('WebGL vendor info not correctly applied');
}
}
console.log('指纹验证成功');
sysLogger.debug('Fingerprint verification successful');
} catch (verifyError) {
console.warn('指纹验证时出错:', verifyError);
sysLogger.warn('Error verifying fingerprint:', verifyError);
}
}
return fingerprint;
} catch (error) {
console.error('设置浏览器指纹时出错:', error);
sysLogger.error('Browser:', error);
throw error;
}
}
/**
* 验证页面指纹是否正确应用
*
* @param {Object} page - Puppeteer
* @param {Object} fingerprint - 指定指纹
* @param {Object} fingerprint -
* @returns {Promise<boolean>}
*/
export async function verifyFingerprint(page, fingerprint) {
@@ -1546,24 +1547,24 @@ export async function verifyFingerprint(page, fingerprint) {
};
}, fingerprint);
console.log('指纹验证结果:', results);
sysLogger.debug('Fingerprint verification result:', results);
return results.success;
} catch (error) {
console.error('验证指纹时出错:', error);
sysLogger.error('Error verifying fingerprint:', error);
return false;
}
}
/**
* 获取随机指纹
*
* @param {string} browserFamily ('chrome', 'edge', 'firefox', 'safari')
* @returns {Object} - 指纹
* @returns {Object} -
*/
export function getRealisticFingerprint(browserFamily = 'chrome') {
// 标准化浏览器
// Browser
browserFamily = browserFamily.toLowerCase();
// 选择合适操作系统
// System
let osFamily;
const browserType = browserFamily;
@@ -1589,7 +1590,7 @@ export function getRealisticFingerprint(browserFamily = 'chrome') {
}
}
// 选择规格
// Select specs
let computerSpec;
if (osFamily === 'Windows' || osFamily === 'Macintosh') {
computerSpec = {
@@ -1608,7 +1609,7 @@ export function getRealisticFingerprint(browserFamily = 'chrome') {
};
}
// 语言
// Language
const commonLanguages = ['en-US', 'en-GB', 'zh-CN', 'es-ES', 'fr-FR', 'de-DE', 'ja-JP', 'ru-RU'];
const locale = randomChoice(commonLanguages);
@@ -2,19 +2,27 @@ import * as docx from "docx";
import cookie from "cookie";
import fs from "fs";
import {execSync} from "child_process";
import sysLogger from './sysLogger.mjs';
let cachedGitRevision = null;
function getGitRevision() {
if (cachedGitRevision) {
return cachedGitRevision;
}
// get git revision and branch
try {
const revision = execSync("git rev-parse --short HEAD", {stdio: "pipe"}).toString().trim();
const branch = execSync("git rev-parse --abbrev-ref HEAD", {stdio: "pipe"}).toString().trim();
return {revision, branch};
cachedGitRevision = {revision, branch};
return cachedGitRevision;
} catch (e) {
return {revision: "unknown", branch: "unknown"};
cachedGitRevision = {revision: "unknown", branch: "unknown"};
return cachedGitRevision;
}
}
// 创建目录
// Create directory
function createDirectoryIfNotExists(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, {recursive: true});
@@ -43,7 +51,7 @@ function extractCookie(cookies) {
function getSessionCookie(jwtSession, jwtToken, ds, dsr, you_subscription, youpro_subscription) {
let sessionCookie = [];
// 处理旧版 cookie
// Process legacy cookie
if (jwtSession && jwtToken) {
sessionCookie = [
{
@@ -89,7 +97,7 @@ function getSessionCookie(jwtSession, jwtToken, ds, dsr, you_subscription, youpr
];
}
// 处理新版 cookie
// Process new cookie
if (ds) {
sessionCookie.push({
name: "DS",
@@ -141,7 +149,7 @@ function getSessionCookie(jwtSession, jwtToken, ds, dsr, you_subscription, youpr
});
}
// 添加隐身模式 cookie(如果启用)
// Add incognito mode cookie (if enabled)
if (process.env.INCOGNITO_MODE === "true") {
sessionCookie.push({
name: "incognito",
@@ -185,50 +193,12 @@ function createEvent(event, data) {
if (typeof data === "object") {
data = JSON.stringify(data);
}
if (event === "data") {
return `data: ${data}\n\n`;
}
return `event: ${event}\ndata: ${data}\n\n`;
}
function extractPerplexityCookie(cookieString) {
const cookies = cookie.parse(cookieString);
return {
sessionToken: cookies['__Secure-next-auth.session-token'],
isIncognito: cookies['pplx.is-incognito'] === 'true'
};
}
function getPerplexitySessionCookie(extractedCookie) {
let sessionCookie = [];
if (extractedCookie.sessionToken) {
sessionCookie.push({
name: "__Secure-next-auth.session-token",
value: extractedCookie.sessionToken,
domain: "www.perplexity.ai",
path: "/",
expires: 1800000000,
httpOnly: true,
secure: true,
sameSite: "Lax",
});
}
// 添加无痕模式 cookie (如果启用)
if (process.env.INCOGNITO_MODE === "true") {
sessionCookie.push({
name: "pplx.is-incognito",
value: "true",
domain: "www.perplexity.ai",
path: "/",
expires: 1800000000,
httpOnly: false,
secure: true,
sameSite: "Lax",
});
}
return sessionCookie;
}
export {
createEvent,
createDirectoryIfNotExists,
@@ -237,6 +207,4 @@ export {
getSessionCookie,
createDocx,
getGitRevision,
extractPerplexityCookie,
getPerplexitySessionCookie
};
@@ -5,23 +5,24 @@ import path from 'path';
import os from 'os';
import {fileURLToPath} from 'url';
import {setupBrowserFingerprint} from './browserFingerprint.mjs';
import sysLogger from './sysLogger.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const execPromise = promisify(exec);
/**
* @param {string} userDataDir - 用户数据目录
* @param {string} edgePath - Edge浏览器路径
* @param {number} debugPort - 调试端口
* @returns {Promise<object>} - browser和page
* @param {string} userDataDir -
* @param {string} edgePath - EdgeBrowser
* @param {number} debugPort -
* @returns {Promise<object>} - browserpage
*/
export async function launchEdgeBrowser(userDataDir, edgePath, debugPort = 9222) {
if (!fs.existsSync(userDataDir)) {
fs.mkdirSync(userDataDir, {recursive: true});
}
// 关闭可能的Edge进程
// Close possible Edge processes
try {
if (os.platform() === 'win32') {
await execPromise('taskkill /f /im msedge.exe').catch(() => {
@@ -52,27 +53,27 @@ export async function launchEdgeBrowser(userDataDir, edgePath, debugPort = 9222)
'--disable-sync',
'--window-size=1280,850',
'--force-device-scale-factor=1',
'about:blank' // 打开空白页
'about:blank' // Open blank page
];
try {
// 启动Edge浏览器
// EdgeBrowser
const cmdArgs = args.join(' ');
const cmd = `"${edgePath}" ${cmdArgs}`;
edgeProcess = exec(cmd);
console.log(`等待Edge浏览器启动...`);
sysLogger.debug(`Waiting for Edge browser to start...`);
await new Promise(resolve => setTimeout(resolve, 3000));
const puppeteer = await import('puppeteer-core');
// 连接到浏览器
// Browser
const browser = await puppeteer.connect({
browserURL: `http://127.0.0.1:${remoteDebuggingPort}`,
defaultViewport: {width: 1280, height: 850}
});
// 获取第一个页面
// Get first page
const pages = await browser.pages();
let page = pages[0];
if (!page) {
@@ -80,17 +81,17 @@ export async function launchEdgeBrowser(userDataDir, edgePath, debugPort = 9222)
}
const originalUserAgent = await page.evaluate(() => navigator.userAgent);
// console.log(`Edge浏览器原始用户代理: ${originalUserAgent}`);
// sysLogger.debug(`EdgeBrowser: ${originalUserAgent}`);
// 应用随机指纹
// Apply random fingerprint
const fingerprint = await setupBrowserFingerprint(page, 'edge');
// 验证指纹是否成功应用
// Verify if fingerprint was successfully applied
const newUserAgent = await page.evaluate(() => navigator.userAgent);
// console.log(`Edge浏览器应用指纹后用户代理: ${newUserAgent}`);
// sysLogger.debug(`EdgeBrowserApply fingerprint: ${newUserAgent}`);
if (!newUserAgent.includes('Edg')) {
console.warn(`警告: 浏览器可能不是Edge。`);
sysLogger.warn(`: BrowserEdge。`);
}
return {
@@ -100,7 +101,7 @@ export async function launchEdgeBrowser(userDataDir, edgePath, debugPort = 9222)
fingerprint: fingerprint
};
} catch (error) {
console.error(`启动Edge浏览器失败:`, error);
sysLogger.error(`EdgeBrowserFailed:`, error);
if (edgeProcess) {
try {
@@ -114,7 +115,7 @@ export async function launchEdgeBrowser(userDataDir, edgePath, debugPort = 9222)
}
/**
* 查找Edge浏览器路径
* EdgeBrowser
* @returns {string|null}
*/
export function findEdgePath() {
+45
View File
@@ -0,0 +1,45 @@
const LOG_LEVELS = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
};
const currentLevel = LOG_LEVELS[(process.env.LOG_LEVEL || 'info').toUpperCase()] ?? LOG_LEVELS.INFO;
const colors = {
reset: "\x1b[0m",
blue: "\x1b[34m",
yellow: "\x1b[33m",
red: "\x1b[31m",
gray: "\x1b[90m"
};
class SysLogger {
debug(...args) {
if (currentLevel <= LOG_LEVELS.DEBUG) {
console.log(colors.gray + "[DEBUG]" + colors.reset, ...args);
}
}
info(...args) {
if (currentLevel <= LOG_LEVELS.INFO) {
console.log(colors.blue + "[INFO]" + colors.reset, ...args);
}
}
warn(...args) {
if (currentLevel <= LOG_LEVELS.WARN) {
console.warn(colors.yellow + "[WARN]" + colors.reset, ...args);
}
}
error(...args) {
if (currentLevel <= LOG_LEVELS.ERROR) {
console.error(colors.red + "[ERROR]" + colors.reset, ...args);
}
}
}
const sysLogger = new SysLogger();
export default sysLogger;
@@ -2,18 +2,19 @@ import fs from "fs";
import path from "path";
import {fileURLToPath} from "url";
import {Mutex} from "async-mutex";
import sysLogger from '../utils/sysLogger.mjs';
const configMutex = new Mutex(); // 互斥锁
const configMutex = new Mutex(); // Mutex lock
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const CONFIG_FILE_PATH = path.join(__dirname, "../config.mjs");
const CONFIG_FILE_PATH = path.join(process.cwd(), "config.mjs");
// 仅在 USE_MANUAL_LOGIN false ENABLE_AUTO_COOKIE_UPDATE true 时生效
// Only effective when USE_MANUAL_LOGIN is false and ENABLE_AUTO_COOKIE_UPDATE is true
const ENABLE_AUTO_COOKIE_UPDATE = process.env.ENABLE_AUTO_COOKIE_UPDATE === "true";
function unifyQuotesForJSON(str) {
// 正则匹配 `` `...` ``
// Regex match `` `...` ``
let out = str.replace(/`([^`]*)`/g, (match, p1) => {
const safe = p1.replace(/"/g, '\\"');
return `"${safe}"`;
@@ -28,8 +29,8 @@ function unifyQuotesForJSON(str) {
/**
* cookies 解析出 DS DSR
* @param {Array} cookies 获取到的 cookie 数组
* cookies DS DSR
* @param {Array} cookies cookie
* @returns {{ ds?: string, dsr?: string }}
*/
function parseDSAndDSR(cookies) {
@@ -45,9 +46,9 @@ function parseDSAndDSR(cookies) {
}
/**
* DS 中解析 email 字段
* DS email
* @param {string} dsToken DS cookie
* @returns {string|null} 返回 email或null
* @returns {string|null} emailnull
*/
function decodeEmailFromDs(dsToken) {
try {
@@ -61,7 +62,7 @@ function decodeEmailFromDs(dsToken) {
}
/**
* cookie 数组转换 "name=value; name=value"
* cookie "name=value; name=value"
* @param {Array} cookies
* @returns {string}
*/
@@ -70,8 +71,8 @@ function cookiesToStringAll(cookies) {
}
/**
* cookie 转换数组
* 每个对象如 { name, value }
* cookie
* { name, value }
* @param {string} cookieStr
* @returns {Array}
*/
@@ -83,9 +84,9 @@ function parseCookieString(cookieStr) {
}
/**
* 本地 configObj.sessions 查找与指定 email 匹配的 session
* @param {object} configObj 解析后 config
* @param {string} email 匹配的邮箱
* configObj.sessions email session
* @param {object} configObj config
* @param {string} email
* @returns {{ index: number, oldCookie: string, ds: string, dsr: string } | null}
*/
function findSessionByEmail(configObj, email) {
@@ -111,14 +112,14 @@ function findSessionByEmail(configObj, email) {
}
/**
* config.mjs 中匹配相同 email session DS DSR 有变化则更新整个 cookie
* config.mjs email session DS DSR cookie
* @param {import('puppeteer-core').Page} page
*/
export async function updateLocalConfigCookieByEmail(page) {
if (!ENABLE_AUTO_COOKIE_UPDATE || process.env.USE_MANUAL_LOGIN === "true") {
return;
}
// 尝试从 “https://you.com/api/instrumentation” 获取 cookie
// Try fetching cookie from https://you.com/api/instrumentation
let cookieStringFromInstrumentation = "";
try {
const instrRequest = await page.waitForRequest(
@@ -135,7 +136,7 @@ export async function updateLocalConfigCookieByEmail(page) {
if (cookieStringFromInstrumentation) {
allCookiesString = cookieStringFromInstrumentation;
} else {
// 使用 page.cookies() 获取
// Fetch using page.cookies()
const cookies = await page.cookies("https://you.com");
allCookiesString = cookiesToStringAll(cookies);
}
@@ -143,24 +144,24 @@ export async function updateLocalConfigCookieByEmail(page) {
const cookieArray = parseCookieString(allCookiesString);
const {ds: newDs, dsr: newDsr} = parseDSAndDSR(cookieArray);
if (!newDs) {
console.log("网页未找到 DS,跳过更新。");
sysLogger.debug("DS not found on page, skipping update.");
return;
}
const newEmail = decodeEmailFromDs(newDs);
if (!newEmail) {
console.log("[网页无法从 DS 解出 email,跳过更新。");
sysLogger.debug("[Page cannot resolve email from DS, skipping update.");
return;
}
// 互斥区
// Mutex area
await configMutex.runExclusive(async () => {
try {
if (!fs.existsSync(CONFIG_FILE_PATH)) {
console.warn(`找不到 config.mjs: ${CONFIG_FILE_PATH}`);
sysLogger.warn(`config.mjs not found: ${CONFIG_FILE_PATH}`);
return;
}
const raw = fs.readFileSync(CONFIG_FILE_PATH, "utf8");
// 去掉 export const config =
// Remove export const config =
let jsonString = raw.replace(/^export const config\s*=\s*/, "").trim();
jsonString = unifyQuotesForJSON(jsonString);
@@ -169,12 +170,12 @@ export async function updateLocalConfigCookieByEmail(page) {
const found = findSessionByEmail(configObj, newEmail);
if (!found) {
console.log(`未能在 config 中找到 email=${newEmail} 的 session,跳过更新。`);
sysLogger.debug(`Could not find session for email=${newEmail} in config, skipping update.`);
return;
}
if (found.ds === newDs && found.dsr === newDsr) {
console.log(`DS/DSR 未变化(email=${newEmail}),不更新。`);
sysLogger.debug(`DS/DSR unchanged (email=${newEmail}), skipping update.`);
return;
}
@@ -183,22 +184,22 @@ export async function updateLocalConfigCookieByEmail(page) {
const newFileContent = "export const config = " + JSON.stringify(configObj, null, 4);
fs.writeFileSync(CONFIG_FILE_PATH, newFileContent, "utf8");
console.log(`Cookie已更新(email=${newEmail})`);
sysLogger.debug(`Cookie updated (email=${newEmail})`);
} catch (err) {
console.warn("Cookie更新过程出错:", err);
sysLogger.warn("Error during cookie update:", err);
}
});
}
/**
* 非阻塞
*
* @param {import('puppeteer-core').Page} page
*/
export function updateLocalConfigCookieByEmailNonBlocking(page) {
// 保证异步
// Ensure async
setImmediate(() => {
updateLocalConfigCookieByEmail(page).catch(err =>
console.error("Cookie update error:", err)
sysLogger.error("Cookie update error:", err)
);
});
}
@@ -1,4 +1,5 @@
import crypto from 'crypto';
import sysLogger from '../utils/sysLogger.mjs';
export function getRandomInt(min, max) {
min = Math.ceil(min);
@@ -6,7 +7,7 @@ export function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// 插入乱码
// Insert garbage code
export function insertGarbledText(content) {
const enableGarbledStart = process.env.ENABLE_GARBLED_START === 'true';
const enableGarbledEnd = process.env.ENABLE_GARBLED_END === 'true';
@@ -15,14 +16,14 @@ export function insertGarbledText(content) {
return content;
}
// 生成指定长度的随机乱码
// Generate random garbage of specified length
function generateGarbledText(length) {
return crypto.randomBytes(length).toString('hex');
}
let garbledContent = content;
// 配置参数
// Configuration parameters
const startMinLength = parseInt(process.env.GARBLED_START_MIN_LENGTH) || 1000;
const startMaxLength = parseInt(process.env.GARBLED_START_MAX_LENGTH) || 5000;
@@ -33,7 +34,7 @@ export function insertGarbledText(content) {
const byteLength = Math.ceil(startGarbledLength / 2);
// 生成乱码
// Generate garbage
const startPlaceholder = generateGarbledText(byteLength);
garbledContent = startPlaceholder + '\n\n\n' + garbledContent.trim();
@@ -2,6 +2,7 @@ import path from "path";
import fs from "fs";
import {fileURLToPath} from 'url';
import {Mutex} from 'async-mutex';
import sysLogger from '../utils/sysLogger.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -9,7 +10,11 @@ const __dirname = path.dirname(__filename);
class Logger {
constructor() {
this.logMutex = new Mutex();
this.logFilePath = path.join(__dirname, 'requests.log');
const dataDir = path.join(process.cwd(), 'data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
this.logFilePath = path.join(dataDir, 'requests.log');
this.statistics = {};
this.monthStart = this.getMonthStart();
this.today = this.getToday();
@@ -18,7 +23,7 @@ class Logger {
getMonthStart() {
const now = new Date();
// 每月第一天
// First days of month
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
monthStart.setHours(0, 0, 0, 0);
return monthStart;
@@ -26,20 +31,22 @@ class Logger {
getToday() {
const now = new Date();
// 获取当天日期
// Get current day date
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
today.setHours(0, 0, 0, 0);
return today;
}
// 加载日志
loadStatistics() {
this.logMutex.runExclusive(() => {
if (!fs.existsSync(this.logFilePath)) {
fs.writeFileSync(this.logFilePath, '', 'utf8');
// Load log
async loadStatistics() {
await this.logMutex.runExclusive(async () => {
try {
await fs.promises.access(this.logFilePath, fs.constants.F_OK);
} catch (err) {
await fs.promises.writeFile(this.logFilePath, '', 'utf8');
return;
}
const data = fs.readFileSync(this.logFilePath, 'utf-8');
const data = await fs.promises.readFile(this.logFilePath, 'utf-8');
const entries = data.split('\n').filter(line => line.trim());
const validEntries = [];
@@ -47,7 +54,7 @@ class Logger {
try {
const logEntry = JSON.parse(line);
// 补全缺少的字段
// Fill in missing fields
if (!logEntry.provider) {
logEntry.provider = 'you';
}
@@ -67,7 +74,7 @@ class Logger {
logEntry.unusualQueryVolume = false;
}
// 调整字段顺序
// Adjust field order
const logEntryArray = [
['provider', logEntry.provider],
['email', logEntry.email],
@@ -81,27 +88,27 @@ class Logger {
validEntries.push(formattedLogEntry);
} catch (e) {
console.warn(`无法解析的日志,已忽略: ${line}`);
sysLogger.warn(`Unparseable log, ignored: ${line}`);
}
}
// 处理有效日志
// Process valid log
for (const logEntry of validEntries) {
const logDate = new Date(logEntry.time);
const provider = logEntry.provider;
const email = logEntry.email;
// 初始化 provider
// Initialize provider
if (!this.statistics[provider]) {
this.statistics[provider] = {};
}
// 初始化邮箱
// Initialize email
if (!this.statistics[provider][email]) {
this.statistics[provider][email] = {
allRequests: [], // 所有请求
monthlyRequests: [], // 本月请求
dailyRequests: [], // 当日请求
allRequests: [], // All requests
monthlyRequests: [], // Requests this month
dailyRequests: [], // Today's requests
monthlyStats: {
totalRequests: 0,
defaultModeCount: 0,
@@ -120,20 +127,20 @@ class Logger {
const stats = this.statistics[provider][email];
stats.allRequests.push(logEntry);
// 本月统计
// This month's statistics
if (logDate >= this.monthStart) {
stats.monthlyRequests.push(logEntry);
this.updateStatistics(stats.monthlyStats, logEntry);
}
// 当日统计
// Today's statistics
if (logDate >= this.today) {
stats.dailyRequests.push(logEntry);
this.updateStatistics(stats.dailyStats, logEntry);
}
}
// 对每个 provider 的每个邮箱时间排序
// Sort time for each email of each provider
for (const provider in this.statistics) {
for (const email in this.statistics[provider]) {
const stats = this.statistics[provider][email];
@@ -143,15 +150,15 @@ class Logger {
}
}
// 清理无效数据
// Clean invalid data
const cleanedData = validEntries.map(entry => JSON.stringify(entry)).join('\n') + '\n';
fs.writeFileSync(this.logFilePath, cleanedData);
await fs.promises.writeFile(this.logFilePath, cleanedData);
}).catch(err => {
console.error('loadStatistics() 加锁异常:', err);
sysLogger.error('loadStatistics() lock exception:', err);
});
}
// 更新统计
// Update statistics
updateStatistics(stats, logEntry) {
stats.totalRequests++;
if (logEntry.mode === 'default') {
@@ -168,10 +175,10 @@ class Logger {
}
}
// 记录请求日志
// Record request log
logRequest({provider, email, time, mode, model, completed, unusualQueryVolume}) {
const logEntryArray = [
['provider', provider || process.env.ACTIVE_PROVIDER || 'you'],
['provider', provider || 'you'],
['email', email || 'unknown'],
['time', time],
['mode', mode || 'unknown'],
@@ -181,9 +188,9 @@ class Logger {
];
const logEntry = Object.fromEntries(logEntryArray);
// 写日志与更新 statistics
this.logMutex.runExclusive(() => {
fs.appendFileSync(this.logFilePath, JSON.stringify(logEntry) + '\n');
// Write log and update statistics
this.logMutex.runExclusive(async () => {
await fs.promises.appendFile(this.logFilePath, JSON.stringify(logEntry) + '\n');
const logDate = new Date(logEntry.time);
const providerName = logEntry.provider;
@@ -215,25 +222,25 @@ class Logger {
const stats = this.statistics[providerName][userEmail];
stats.allRequests.push(logEntry);
// 当日统计
// Today's statistics
if (logDate >= this.today) {
stats.dailyRequests.push(logEntry);
this.updateStatistics(stats.dailyStats, logEntry);
}
// 本月统计
// This month's statistics
if (logDate >= this.monthStart) {
stats.monthlyRequests.push(logEntry);
this.updateStatistics(stats.monthlyStats, logEntry);
}
}).catch(err => {
console.error('logRequest() 加锁异常:', err);
sysLogger.error('logRequest() lock exception:', err);
});
}
// 输出当前统计信息
// Output current statistics
printStatistics() {
const provider = process.env.ACTIVE_PROVIDER || 'you';
const provider = 'you';
const monthStartStr = this.monthStart.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
@@ -246,46 +253,46 @@ class Logger {
});
if (!this.statistics[provider]) {
console.log(`===== 提供者 ${provider} 没有统计数据 =====`);
sysLogger.info(`===== Provider ${provider} has no statistics =====`);
return;
}
const emails = Object.keys(this.statistics[provider]).sort();
let hasAnyDailyRequest = false;
console.log(`===== 请求统计信息 (Provider=${provider}) =====`);
sysLogger.info(`===== Request Statistics (Provider=${provider}) =====`);
for (const email of emails) {
const stats = this.statistics[provider][email];
// 当日是否有请求
// Any requests today
if (stats.dailyStats.totalRequests > 0) {
hasAnyDailyRequest = true;
console.log(`用户邮箱: ${email}`);
console.log(`---------- 本月[自 ${monthStartStr} ] 统计 ----------`);
console.log(`总请求次数: ${stats.monthlyStats.totalRequests}`);
console.log(`default 请求次数: ${stats.monthlyStats.defaultModeCount}`);
console.log(`custom 请求次数: ${stats.monthlyStats.customModeCount}`);
console.log('各模型请求次数:');
sysLogger.debug(`User email: ${email}`);
sysLogger.info(`---------- This Month [Since ${monthStartStr} ] Statistics ----------`);
sysLogger.info(`Total requests: ${stats.monthlyStats.totalRequests}`);
sysLogger.info(`Default requests: ${stats.monthlyStats.defaultModeCount}`);
sysLogger.info(`Custom requests: ${stats.monthlyStats.customModeCount}`);
sysLogger.info('Requests by model:');
for (const [mdl, count] of Object.entries(stats.monthlyStats.modelCount)) {
console.log(` - ${mdl}: ${count}`);
sysLogger.debug(` - ${mdl}: ${count}`);
}
console.log(`---------- 今日[${todayStr}]统计 ----------`);
console.log(`总请求次数: ${stats.dailyStats.totalRequests}`);
console.log(`default 请求次数: ${stats.dailyStats.defaultModeCount}`);
console.log(`custom 请求次数: ${stats.dailyStats.customModeCount}`);
console.log('各模型请求次数:');
sysLogger.info(`---------- Today [${todayStr}] Statistics ----------`);
sysLogger.info(`Total requests: ${stats.dailyStats.totalRequests}`);
sysLogger.info(`Default requests: ${stats.dailyStats.defaultModeCount}`);
sysLogger.info(`Custom requests: ${stats.dailyStats.customModeCount}`);
sysLogger.info('Requests by model:');
for (const [mdl, count] of Object.entries(stats.dailyStats.modelCount)) {
console.log(` - ${mdl}: ${count}`);
sysLogger.debug(` - ${mdl}: ${count}`);
}
console.log('------------------------------');
sysLogger.debug('------------------------------');
}
}
if (!hasAnyDailyRequest) {
console.log(`===== 今日(${todayStr})无任何账号发生请求 =====`);
sysLogger.info(`===== No account requests today (${todayStr}) =====`);
}
console.log('================================');
sysLogger.debug('================================');
}
}
File diff suppressed because it is too large Load Diff
+49 -49
View File
@@ -1,119 +1,119 @@
@echo off
REM 安装依赖包
REM Install dependencies
call npm install
REM 设置代理的网站:youperplexityhappyapi
REM Set the website of the proxy: you, perplexity, happyapi
set ACTIVE_PROVIDER=you
REM 设置指定浏览器,可以是 'chrome', 'edge' 'auto'
REM Set the browser type, can be 'chrome', 'edge', or 'auto'
set BROWSER_TYPE=auto
REM 设置是否启用手动登录
REM Set whether to enable manual login
set USE_MANUAL_LOGIN=true
REM 设置是否隐藏浏览器 (设置浏览器实例较大时,建议设置为true) (只有在`USE_MANUAL_LOGIN=false`时才有效)
REM Set whether to hide the browser (recommended to set to true when the browser instance is large) (only effective when `USE_MANUAL_LOGIN=false`)
set HEADLESS_BROWSER=true
REM 设置启动浏览器实例数量(非并发场景下,建议设置1)
REM Set the number of browser instances to start (recommended to set to 1 in non-concurrent scenarios)
set BROWSER_INSTANCE_COUNT=1
REM 设置会话自动释放时间(单位:秒) (0=禁用自动释放)
REM Set the session auto-release time (in seconds) (0 = disable auto-release)
set SESSION_LOCK_TIMEOUT=180
REM 设置是否启用并发限制
REM Set whether to enable detection
set ENABLE_DETECTION=true
REM 设置是否启用自动Cookie更新 (USE_MANUAL_LOGIN=false时有效)
REM Set whether to enable automatic cookie updates (effective only when `USE_MANUAL_LOGIN=false`)
set ENABLE_AUTO_COOKIE_UPDATE=false
REM 是否跳过账户验证 (启用时,`ALLOW_NON_PRO`设置无效,可用于账号量多情况)
REM Set whether to skip account verification (when enabled, `ALLOW_NON_PRO` is ignored, can be used in scenarios with multiple accounts)
set SKIP_ACCOUNT_VALIDATION=false
REM 开启请求次数上限(默认限制3次请求) (用于免费账户)
REM Set whether to enable request limits (default limit is 3 requests) (for free accounts)
set ENABLE_REQUEST_LIMIT=false
REM 是否允许非Pro账户
REM Set whether to allow non-Pro accounts
set ALLOW_NON_PRO=false
REM 设置自定义终止符(用于处理输出停不下来情况,留空则不启用,使用双引号包裹)
REM Set the custom end marker (used to handle situations where the output does not stop, leave blank to not use, use double quotes to enclose)
set CUSTOM_END_MARKER="<CHAR_turn>"
REM 设置是否启用延迟发送请求,如果设置false卡发送请求尝试打开它
REM Set whether to enable delayed sending of requests, if set to false, it will attempt to open the request
set ENABLE_DELAY_LOGIC=false
REM 设置是否启用隧道访问
REM Set whether to enable tunnel access
set ENABLE_TUNNEL=false
REM 设置隧道类型 (localtunnel ngrok)
REM Set the tunnel type (localtunnel or ngrok)
set TUNNEL_TYPE=ngrok
REM 设置localtunnel子域名(留空则为随机域名)
REM Set the localtunnel subdomain (leave blank for a random domain)
set SUBDOMAIN=
REM 设置 ngrok AUTH TOKEN
REM 这是 ngrok 账户的身份验证令牌。可以在 ngrok 仪表板的 "Auth" 部分找到它。
REM 免费账户和付费账户都需要设置此项。
REM ngrok网站: https://dashboard.ngrok.com
REM Set the ngrok authentication token
REM This is the authentication token for the ngrok account, which can be found in the "Auth" section of the ngrok dashboard.
REM Both free and paid accounts require this to be set.
REM ngrok website: https://dashboard.ngrok.com
set NGROK_AUTH_TOKEN=
REM 设置 ngrok 自定义域名
REM 这允许使用自己的域名而不是 ngrok 的随机子域名。
REM 注意:此功能仅适用于 ngrok 付费账户。
REM 使用此功能前,请确保已在 ngrok 仪表板中添加并验证了该域名。
REM 格式示例:your-custom-domain.com
REM 如果使用免费账户或不想使用自定义域名,请将此项留空。
REM Set the ngrok custom domain
REM This allows you to use your own domain instead of ngrok's random subdomain.
REM Note: This feature is only available for ngrok paid accounts.
REM Before using this feature, make sure you have added and verified the domain in the ngrok dashboard.
REM Example format: your-custom-domain.com
REM If using a free account or not using a custom domain, leave this blank.
set NGROK_CUSTOM_DOMAIN=
REM 设置 https_proxy 代理,可以使用本地的socks5http(s)代理
REM 例如,使用 HTTP 代理:export https_proxy=http://127.0.0.1:7890
REM 或者使用 SOCKS5 代理:export https_proxy=socks5://host:port:username:password
REM Set the https_proxy proxy, can use local socks5 or http(s) proxy
REM For example, using an HTTP proxy: export https_proxy=http://127.0.0.1:7890
REM Or using a SOCKS5 proxy: export https_proxy=socks5://host:port:username:password
set https_proxy=
REM 设置 PASSWORD API密码
REM Set the PASSWORD API password
set PASSWORD=
REM 设置 PORT 端口
REM Set the PORT port
set PORT=8080
REM 设置AI模型(Claude系列模型直接在酒馆中选择即可使用,修改`AI_MODEL`环境变量可以切换Claude以外的模型,支持的模型名字如下 (请参考官网获取最新模型))
REM Set the AI model (Claude series models can be used directly in the tavern, modifying the `AI_MODEL` environment variable can switch to other models, supported model names are as follows (please refer to the official website for the latest models))
set AI_MODEL=
REM 自定义会话模式
REM Set custom session mode
set USE_CUSTOM_MODE=false
REM 启用模式轮换
REM 只有当 USE_CUSTOM_MODEENABLE_MODE_ROTATION 都设置为 true 时,才会启用模式轮换功能。
REM 可以在自定义模式和默认模式之间动态切换
REM Enable mode rotation
REM Mode rotation is only enabled when both `USE_CUSTOM_MODE` and `ENABLE_MODE_ROTATION` are set to true.
REM Can dynamically switch between custom mode and default mode
set ENABLE_MODE_ROTATION=false
REM 是否启用隐身模式
REM Set whether to enable incognito mode
set INCOGNITO_MODE=false
REM 设置伪造真role (如果启用,必须使用txt格式上传)
REM Set whether to fake the true role (if enabled, must use txt format for upload)
set USE_BACKSPACE_PREFIX=false
REM 设置上传文件格式 (docx txt) gpt_4o 使用txt可能更好破限
REM Set the upload file format (docx or txt) gpt_4o may be better with txt
set UPLOAD_FILE_FORMAT=txt
REM 设置是否启用 CLEWD 后处理
REM Set whether to enable CLEWD post-processing
set CLEWD_ENABLED=false
REM ---------------------------------------------------
REM 控制是否在开头插入乱码
REM Set whether to insert garbled characters at the beginning
set ENABLE_GARBLED_START=false
REM 设置开头插入乱码最小长度
REM Set the minimum length of garbled characters to insert at the beginning
set GARBLED_START_MIN_LENGTH=1000
REM 设置开头插入乱码最大长度
REM Set the maximum length of garbled characters to insert at the beginning
set GARBLED_START_MAX_LENGTH=5000
REM 设置结尾插入乱码固定长度
REM Set the fixed length of garbled characters to insert at the end
set GARBLED_END_LENGTH=500
REM 控制是否在结尾插入乱码
REM Set whether to insert garbled characters at the end
set ENABLE_GARBLED_END=false
REM ---------------------------------------------------
REM 运行 Node.js 应用程序
node index.mjs
REM Run the Node.js application
node src/index.mjs
REM 暂停脚本执行,等待用户按任意键退出
REM Pause script execution, waiting for the user to press any key to exit
pause
Regular → Executable
+46 -46
View File
@@ -1,112 +1,112 @@
#!/bin/bash
# 安装依赖包
# Install dependencies
npm install
# 设置代理的网站:youperplexityhappyapi
# Set proxy provider: you, perplexity, happyapi
export ACTIVE_PROVIDER=you
# 设置指定浏览器,可以是 'chrome', 'edge' 'auto'
# Set specified browser, can be 'chrome', 'edge' or 'auto'
export BROWSER_TYPE=auto
# 设置是否启用手动登录
# Set whether to enable manual login
export USE_MANUAL_LOGIN=true
# 设置是否隐藏浏览器 (设置浏览器实例较大时,建议设置为true) (只有在`USE_MANUAL_LOGIN=false`时才有效)
# Set whether to hide browser (recommended to set to true when browser instance count is large) (only effective when `USE_MANUAL_LOGIN=false`)
export HEADLESS_BROWSER=true
# 设置启动浏览器实例数量(非并发场景下,建议设置1)
# Set number of browser instances to launch (recommended to set 1 for non-concurrent scenarios)
export BROWSER_INSTANCE_COUNT=1
# 设置会话自动释放时间(单位:秒) (0=禁用自动释放)
# Set session auto-release time (unit: seconds) (0=disable auto-release)
export SESSION_LOCK_TIMEOUT=180
# 设置是否启用并发限制
# Set whether to enable concurrency limit
export ENABLE_DETECTION=true
# 设置是否启用自动Cookie更新 (USE_MANUAL_LOGIN=false时有效)
# Set whether to enable auto cookie update (effective when USE_MANUAL_LOGIN=false)
export ENABLE_AUTO_COOKIE_UPDATE=false
# 是否跳过账户验证 (启用时,`ALLOW_NON_PRO`设置无效,可用于账号量多情况)
# Whether to skip account validation (when enabled, `ALLOW_NON_PRO` setting is invalid, usable for large account quantities)
export SKIP_ACCOUNT_VALIDATION=false
# 开启请求次数上限(默认限制3次请求) (用于免费账户)
# Enable request count limit (default limit 3 requests) (for free accounts)
export ENABLE_REQUEST_LIMIT=false
# 是否允许非Pro账户
# Whether to allow non-Pro accounts
export ALLOW_NON_PRO=false
# 设置自定义终止符(用于处理输出停不下来情况,留空则不启用,使用双引号包裹)
# Set custom terminator (for handling output that won't stop, leave empty to disable, wrap in double quotes)
export CUSTOM_END_MARKER="<CHAR_turn>"
# 设置是否启用延迟发送请求,如果设置false卡发送请求尝试打开它
# Set whether to enable delayed request sending, if set to false and requests get stuck try enabling it
export ENABLE_DELAY_LOGIC=false
# 设置是否启用隧道访问
# Set whether to enable tunnel access
export ENABLE_TUNNEL=false
# 设置隧道类型 (localtunnel ngrok)
# Set tunnel type (localtunnel or ngrok)
export TUNNEL_TYPE=ngrok
# 设置localtunnel子域名(留空则为随机域名)
# Set localtunnel subdomain (leave empty for random domain)
export SUBDOMAIN=
# 设置 ngrok AUTH TOKEN
# 这是 ngrok 账户的身份验证令牌。可以在 ngrok 仪表板的 "Auth" 部分找到它。
# 免费账户和付费账户都需要设置此项。
# ngrok网站: https://dashboard.ngrok.com
# Set ngrok AUTH TOKEN
# This is the authentication token for your ngrok account. You can find it in the "Auth" section of the ngrok dashboard.
# Both free and paid accounts need to set this.
# ngrok website: https://dashboard.ngrok.com
export NGROK_AUTH_TOKEN=
# 设置 ngrok 自定义域名
# 这允许使用自己的域名而不是 ngrok 的随机子域名。
# 注意:此功能仅适用于 ngrok 付费账户。
# 使用此功能前,请确保已在 ngrok 仪表板中添加并验证了该域名。
# 格式示例:your-custom-domain.com
# 如果使用免费账户或不想使用自定义域名,请将此项留空。
# Set ngrok custom domain
# This allows using your own domain instead of ngrok's random subdomain.
# Note: This feature only works with ngrok paid accounts.
# Before using this feature, ensure you have added and verified the domain in the ngrok dashboard.
# Format example: your-custom-domain.com
# If using a free account or don't want to use a custom domain, leave this empty.
export NGROK_CUSTOM_DOMAIN=
# 设置 https_proxy 代理,可以使用本地的socks5http(s)代理
# 例如,使用 HTTP 代理:export https_proxy=http://127.0.0.1:7890
# 或者使用 SOCKS5 代理:export https_proxy=socks5://host:port:username:password
# Set https_proxy proxy, can use local socks5 or http(s) proxy
# For example, using HTTP proxy: export https_proxy=http://127.0.0.1:7890
# Or using SOCKS5 proxy: export https_proxy=socks5://host:port:username:password
export https_proxy=
# 设置 PASSWORD API密码
# Set PASSWORD API password
export PASSWORD=
# 设置 PORT 端口
# Set PORT port
export PORT=8080
# 设置AI模型(Claude系列模型直接在酒馆中选择即可使用,修改`AI_MODEL`环境变量可以切换Claude以外的模型,支持的模型名字如下 (请参考官网获取最新模型))
# Set AI model (Claude series models can be selected directly in Tavern, modifying `AI_MODEL` environment variable can switch to models other than Claude, supported model names below (please refer to official website for latest models))
export AI_MODEL=
# 自定义会话模式
# Custom session mode
export USE_CUSTOM_MODE=false
# 启用模式轮换
# 只有当 USE_CUSTOM_MODE ENABLE_MODE_ROTATION 都设置为 true 时,才会启用模式轮换功能。
# 可以在自定义模式和默认模式之间动态切换
# Enable mode rotation
# Mode rotation is only enabled when both USE_CUSTOM_MODE and ENABLE_MODE_ROTATION are set to true.
# Can dynamically switch between custom mode and default mode
export ENABLE_MODE_ROTATION=false
# 是否启用隐身模式
# Whether to enable incognito mode
export INCOGNITO_MODE=false
# 设置上传文件格式 (docx txt) gpt_4o 使用txt可能更好破限
# Set upload file format (docx or txt) gpt_4o may work better with txt for bypassing limits
export UPLOAD_FILE_FORMAT=docx
# ---------------------------------------------------
# 控制是否在开头插入乱码
# Control whether to insert garbled text at the beginning
export ENABLE_GARBLED_START=false
# 设置开头插入乱码最小长度
# Set minimum length of garbled text at the beginning
export GARBLED_START_MIN_LENGTH=1000
# 设置开头插入乱码最大长度
# Set maximum length of garbled text at the beginning
export GARBLED_START_MAX_LENGTH=5000
# 设置结尾插入乱码固定长度
# Set fixed length of garbled text at the end
export GARBLED_END_LENGTH=500
# 控制是否在结尾插入乱码
# Control whether to insert garbled text at the end
export ENABLE_GARBLED_END=false
# ---------------------------------------------------
# 运行 Node.js 应用程序
node index.mjs
# Run Node.js application
node src/index.mjs
read -p "Press any key to exit..."
+2 -299
View File
@@ -1,300 +1,3 @@
# 使用指南 / Usage Guide
## 前提条件 / Prerequisites
1. **安装必要的软件:**
- Node.js
- Git
- Python
2. **获得一个 You.com 账户并订阅 Pro 或 Team 计划,登录账户。**
3. **建议全局代理来确保网络连接稳定。**
4. **如果需要,可以在 `start.bat` 文件中设置代理。**
---
## 设置步骤 / Setup Steps
### 方法一:使用 Cookie 登录(默认情况下)
#### 步骤 1:获取 Cookie
1. **打开浏览器,登录 [you.com](https://you.com)。**
2. **按 `F12` 打开开发者工具,找到 "Console"(控制台)选项卡。**
3. **在控制台中输入以下代码并回车,然后复制所有输出内容(Cookie):**
```javascript
console.log(document.cookie);
```
#### 步骤 2:配置项目
1. **下载或克隆本项目代码,解压缩。**
2. **编辑 `config.example.mjs` 文件,将上一步获取的 Cookie 粘贴进去。**
如果有多个 Cookie,按照以下格式添加,然后将文件另存为 `config.mjs`
```javascript
export const config = {
"sessions": [
{
"cookie": `cookie1`
},
{
"cookie": `cookie2`
},
{
"cookie": `cookie3`
}
]
}
```
#### 步骤 3:配置环境变量
1. **打开 `start.bat` 文件,根据需要设置环境变量。**
#### 步骤 4:启动服务
1. **双击运行 `start.bat`。**
2. **等待程序安装依赖并启动服务。**
#### 步骤 5:配置客户端
1. **在 SillyTavern 中选择 **Custom (OpenAI-compatible)**。**
2. **将反向代理地址设置为 `http://127.0.0.1:8080/v1`。**
3. **反代密码需要填写(随便填一个即可,除非在 `start.bat` 中设置了 `PASSWORD`)。**
4. **开始使用。如果失败或没有结果,尝试多次重试。**
---
### 方法二:使用手动登录
#### 步骤 1:配置 `start.bat`
1. **打开 `start.bat` 文件,将 `USE_MANUAL_LOGIN` 设置为 `true`**
```batch
set USE_MANUAL_LOGIN=true
```
2. **保存并关闭 `start.bat` 文件。**
#### 步骤 2:启动服务并手动登录
1. **重命名 `config.example.mjs` 文件,将文件另存为 `config.mjs`。**
2. **双击运行 `start.bat`。**
3. **程序将启动并自动打开浏览器窗口。**
4. **在弹出的浏览器窗口中手动登录的 You.com 账户。**
5. **登录成功后,程序将自动获取的会话信息。**
#### 步骤 3:配置客户端
*同方法一的步骤5*
---
## 可选配置 / Optional Configurations
### 设置代理 / Set Proxy
如果需要设置代理,请在 `start.bat` 中设置 `http_proxy` 和 `https_proxy` 环境变量。例如:
```batch
set http_proxy=http://127.0.0.1:7890
set https_proxy=http://127.0.0.1:7890
```
*(启动浏览器闪退时,移除代理)*
This project uses the local Chrome browser, which will automatically read and use the system proxy settings.
### 设置 AI 模型 / Set AI Model
可以通过设置 `AI_MODEL` 环境变量来切换使用的模型。支持的模型包括(请参考官网获取最新模型):
- `gpt_4o`
- `gpt_4_turbo`
- `gpt_4`
- `claude_3_5_sonnet`
- `claude_3_opus`
- `claude_3_sonnet`
- `claude_3_haiku`
- `claude_2`
- `llama3`
- `gemini_pro`
- `gemini_1_5_pro`
- `databricks_dbrx_instruct`
- `command_r`
- `command_r_plus`
- `zephyr`
例如:
```batch
set AI_MODEL=claude_3_opus
```
### 启用自定义会话模式 / Enable Custom Chat Mode
启用后,可以缩短系统消息长度、禁用联网、减少等待时间,可能有助于突破限制。
```batch
set USE_CUSTOM_MODE=true
```
### 启用模式轮换 / Enable Mode Rotation
只有当 `USE_CUSTOM_MODE` 和 `ENABLE_MODE_ROTATION` 都设置为 `true` 时,才会启用模式轮换功能。
```batch
set ENABLE_MODE_ROTATION=true
```
### 启用隧道访问 / Enable Tunnel Access
如果需要从外网访问本地服务,可以启用隧道访问。支持 `ngrok` 和 `localtunnel`。
**使用 ngrok**
1. **设置隧道类型:**
```batch
set ENABLE_TUNNEL=true
set TUNNEL_TYPE=ngrok
```
2. **设置 ngrok Auth Token(从 ngrok 仪表板获取):**
```batch
set NGROK_AUTH_TOKEN=your_ngrok_auth_token
```
3. **(可选)设置自定义域名(付费账户):**
```batch
set NGROK_CUSTOM_DOMAIN=your_custom_domain
```
**使用 localtunnel**
1. **设置隧道类型:**
```batch
set ENABLE_TUNNEL=true
set TUNNEL_TYPE=localtunnel
```
2. **(可选)设置子域名:**
```batch
set SUBDOMAIN=your_subdomain
```
---
## 注意事项 / Important Notes
- **关于 Cloudflare 人机验证:**
如果在程序运行过程中弹出人机验证提示,请在30秒内完成验证。
- **关于 `ALLOW_NON_PRO` 设定:**
如果设置为 `true`,允许使用非订阅账户,但功能会受限,可能无法正常使用。
```batch
set ALLOW_NON_PRO=true
```
- **关于 `CUSTOM_END_MARKER` 设定:**
当输出无法停止时,可设置自定义终止符,程序检测到该终止符后将自动停止输出。
```batch
set CUSTOM_END_MARKER="<YOUR_END_MARKER>"
```
- **关于 `ENABLE_DELAY_LOGIC` 设定:**
如果请求卡顿,尝试将其设置为 `true`。
```batch
set ENABLE_DELAY_LOGIC=true
```
- **关于上传文件格式:**
可以选择上传文件的格式为 `docx` 或 `txt`。
```batch
set UPLOAD_FILE_FORMAT=docx
```
- **关于403问题(基本只存在于旧版本)**
这个问题基本只存在于旧版本,新版本由于使用了浏览器模拟访问,已经不容易被拦截。
新版本如果弹出人机验证提示,用户只需要在30秒内点击完成CloudFlare的人机验证,并且等待程序继续处理即可。
cloudflare有一个风控分数。这个和你的TLS指纹、浏览器指纹、IP地址声誉等等有关系
我们这个项目一直用的TLS指纹和浏览器指纹就非常可疑(都是自动化库和Node内置TLS),分数直接拉满
相当于已经预先有了30+30分数,剩下就看IP地址声誉(40分)你拿了几分
(具体分数不详,只是举个例子)
那如果你IP确实白,拿了0分,那你总共分数就是60。
假设you那边设置了分数高于80的要跳验证码,那现在就没事
如果你IP黑,拿了超过20分,那你就是>80分,你就要跳验证码,结果就是403
然后最近you觉得被薅狠了,或者别的啥原因,把这个分数设置成60以上的就要跳验证码
结果就我IP有点黑,不管怎么搞都过不去了。
但是同样的IP,你用正常的Google Chrome访问,就没问题,因为它的指纹非常干净,所以前面的指纹分数就很低
就算加上IP声誉分他也没到那条线
总之以上是一个简化的版本,CF抗bot还有很多指标、很多策略
---
## 在 Linux 上部署 / Deploy on Linux
可以使用 Docker 进行部署,请参照项目中的 `Dockerfile`。
---
## 常见问题 / FAQ
**Q:** 如何解决 npm 安装依赖失败的问题?
**A:** 请确保的网络连接稳定,必要时使用全局代理。
**Q:** 为什么程序提示 "两种模式均达到请求上限"?
**A:** 这可能是因为频繁请求导致模式被暂时禁用,建议稍等一段时间再尝试。
**Q:** 如何切换模型?
**A:** 编辑 `start.bat` 中的 `AI_MODEL` 环境变量,设置为想使用的模型名称(已经可以在SillyTavern设置了)。
---
## 免责声明 / Disclaimer
本项目仅供学习和研究使用,请遵守相关法律法规,勿用于任何商业或非法用途。
This project is for learning and research purposes only. Please comply with relevant laws and regulations and do not use it for any commercial or illegal purposes.
---
# Usage Guide
## Prerequisites
@@ -416,6 +119,7 @@ set https_proxy=http://127.0.0.1:7890
```
This project uses the local Chrome browser, which will automatically read and use the system proxy settings.
*(If the browser crashes on startup, remove the proxy.)*
### Set AI Model
@@ -538,6 +242,7 @@ If you need to access the local service from the external network, you can enabl
```batch
set UPLOAD_FILE_FORMAT=docx
```
- **About 403 issue (mainly exists in old versions)**
This issue mainly exists in old versions. The new version is less likely to be blocked as it uses browser simulation for access.
@@ -584,5 +289,3 @@ You can deploy using Docker, please refer to the `Dockerfile` in the project.
## Disclaimer
This project is for learning and research purposes only. Please comply with relevant laws and regulations and do not use it for any commercial or illegal purposes.
---
File diff suppressed because it is too large Load Diff