refactor: restructure codebase for v1 kasm docker release
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
browser_profiles
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
*.tar
|
||||||
+3
-1
@@ -6,4 +6,6 @@ test.js
|
|||||||
test.mjs
|
test.mjs
|
||||||
config.js
|
config.js
|
||||||
config.mjs
|
config.mjs
|
||||||
browser_profiles
|
browser_profiles
|
||||||
|
*.log
|
||||||
|
local_copy_formatted_messages.txt
|
||||||
+45
-27
@@ -1,37 +1,55 @@
|
|||||||
# Use the official Node.js 20 image as the base image
|
FROM kasmweb/chrome:1.19.0
|
||||||
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/*
|
|
||||||
|
|
||||||
# Install Google Chrome
|
USER root
|
||||||
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/*
|
|
||||||
|
|
||||||
# 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
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package.json and package-lock.json to the working directory
|
# Copy application files (ignoring node_modules and browser_profiles)
|
||||||
COPY package*.json ./
|
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
|
RUN npm install
|
||||||
|
|
||||||
# Copy the rest of the application code
|
# Create a custom entrypoint script
|
||||||
COPY . .
|
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
|
USER 1000
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
# 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"]
|
||||||
@@ -1,28 +1,24 @@
|
|||||||
# 更新日志(2025年4月4日)
|
# Changelog (April 4, 2025)
|
||||||
添加对gemini 2.5 pro的支持(与官方api相比不支持联网搜索)
|
Added support for gemini 2.5 pro (does not support internet search compared to the official API)
|
||||||
|
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
|
|
||||||
# Miaomiaomiao
|
# Miaomiaomiao
|
||||||
|
|
||||||
A proxy for YOU Chat.
|
A proxy for YOU Chat.
|
||||||
|
|
||||||
把 YOU.Com 转换为通用代理。
|
Convert YOU.Com into a generic proxy.
|
||||||
|
|
||||||
如果您觉得本项目对您有帮助,请考虑[请我喝杯蜜雪冰城](https://github.com/sponsors/Archeb?frequency=one-time)
|
If you find this project useful, please consider [buying me a cup of coffee](https://github.com/sponsors/Archeb?frequency=one-time);
|
||||||
|
|
||||||
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.**
|
||||||
|
|
||||||
**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 ↓
|
||||||
|
|
||||||
ASK THE ASSISTANT TO HELP YOU DEPLOY ↓
|
If you have deployment issues, ask the deployment assistant: https://www.coze.com/store/bot/7383564439096131592?bot_id=true
|
||||||
|
|
||||||
有部署问题请问部署助手: 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.
|
||||||
|
|
||||||
您的电脑上必须安装有 Google Chrome 或者 Edge 浏览器才能够使用当前分支。旧版已不再维护。
|
|
||||||
|
|
||||||
Google Chrome must be installed on your system in order to use this proxy. Previous versions are no longer maintained.
|
|
||||||
|
|||||||
+6
-6
@@ -1,7 +1,7 @@
|
|||||||
export const config = {
|
export const config = {
|
||||||
"sessions": [
|
"sessions": [
|
||||||
{
|
{
|
||||||
"cookie": `...`,
|
"cookie": `...`,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -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
|
||||||
@@ -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;
|
|
||||||
@@ -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
@@ -2,8 +2,9 @@
|
|||||||
"name": "youchat-proxy",
|
"name": "youchat-proxy",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "src/index.mjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"start": "node src/index.mjs",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -11,27 +12,16 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"cookie": "^0.6.0",
|
"cookie": "^0.6.0",
|
||||||
"cookie-parser": "^1.4.7",
|
|
||||||
"crypto-js": "^4.2.0",
|
|
||||||
"dayjs": "^1.11.13",
|
|
||||||
"docx": "^8.5.0",
|
"docx": "^8.5.0",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"form-data": "^4.0.0",
|
|
||||||
"geoip-lite": "^1.4.10",
|
"geoip-lite": "^1.4.10",
|
||||||
"https-proxy-agent": "^7.0.5",
|
"https-proxy-agent": "^7.0.5",
|
||||||
"localtunnel": "^2.0.2",
|
"localtunnel": "^2.0.2",
|
||||||
"ngrok": "^5.0.0-beta.2",
|
"ngrok": "^5.0.0-beta.2",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"puppeteer-core": "^24.4.0",
|
"puppeteer-core": "^24.4.0",
|
||||||
"puppeteer-real-browser": "^1.4.2",
|
|
||||||
"socket.io-client": "^4.8.0",
|
|
||||||
"socks-proxy-agent": "^8.0.4",
|
"socks-proxy-agent": "^8.0.4",
|
||||||
"unzipper": "^0.12.3",
|
"uuid": "^9.0.1"
|
||||||
"uuid": "^9.0.1",
|
|
||||||
"winston": "^3.17.0",
|
|
||||||
"ws": "^8.18.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"playwright": "^1.51.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
export const config = {
|
|
||||||
sessions: [
|
|
||||||
{
|
|
||||||
cookie: `...`
|
|
||||||
}
|
|
||||||
// 可以添加更多cookie
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
@@ -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;
|
|
||||||
@@ -1,428 +1,429 @@
|
|||||||
export function formatMessages(messages, proxyModel, randomFileName) {
|
import sysLogger from './utils/sysLogger.mjs';
|
||||||
// 检查是否是 Claude 模型
|
export function formatMessages(messages, proxyModel, randomFileName) {
|
||||||
const isClaudeModel = proxyModel.toLowerCase().includes('claude');
|
// Check if it is a Claude model
|
||||||
|
const isClaudeModel = proxyModel.toLowerCase().includes('claude');
|
||||||
// 启用特殊前缀
|
|
||||||
const USE_BACKSPACE_PREFIX = process.env.USE_BACKSPACE_PREFIX === 'true';
|
// Enable special prefix
|
||||||
|
const USE_BACKSPACE_PREFIX = process.env.USE_BACKSPACE_PREFIX === 'true';
|
||||||
// 定义角色映射
|
|
||||||
const roleFeatures = getRoleFeatures(messages, isClaudeModel, USE_BACKSPACE_PREFIX);
|
// Define role mapping
|
||||||
|
const roleFeatures = getRoleFeatures(messages, isClaudeModel, USE_BACKSPACE_PREFIX);
|
||||||
// 清除第一条消息的 role
|
|
||||||
messages = clearFirstMessageRole(messages);
|
// Clear role of first message
|
||||||
|
messages = clearFirstMessageRole(messages);
|
||||||
messages = removeCustomRoleDefinitions(messages);
|
|
||||||
|
messages = removeCustomRoleDefinitions(messages);
|
||||||
messages = convertRoles(messages, roleFeatures);
|
|
||||||
|
messages = convertRoles(messages, roleFeatures);
|
||||||
// 替换 content 中的角色
|
|
||||||
messages = replaceRolesInContent(messages, roleFeatures);
|
// Replace role in content
|
||||||
|
messages = replaceRolesInContent(messages, roleFeatures);
|
||||||
// 如果启用 clewd
|
|
||||||
const CLEWD_ENABLED = process.env.CLEWD_ENABLED === 'true';
|
// If clewd is enabled
|
||||||
if (CLEWD_ENABLED) {
|
const CLEWD_ENABLED = process.env.CLEWD_ENABLED === 'true';
|
||||||
messages = xmlPlotAllMessages(messages, roleFeatures);
|
if (CLEWD_ENABLED) {
|
||||||
}
|
messages = xmlPlotAllMessages(messages, roleFeatures);
|
||||||
|
}
|
||||||
messages = messages.map((message) => {
|
|
||||||
let newMessage = { ...message };
|
messages = messages.map((message) => {
|
||||||
if (typeof newMessage.content === 'string') {
|
let newMessage = { ...message };
|
||||||
// 1) 移除 </FORMAT LINE BREAK/> 标识
|
if (typeof newMessage.content === 'string') {
|
||||||
let tempContent = newMessage.content.replace(/<\/FORMAT\s+LINE\s+BREAK\/>/g, '');
|
// 1) Remove </FORMAT LINE BREAK/> tag
|
||||||
tempContent = tempContent.replace(/\n{3,}/g, '\n\n');
|
let tempContent = newMessage.content.replace(/<\/FORMAT\s+LINE\s+BREAK\/>/g, '');
|
||||||
newMessage.content = tempContent;
|
tempContent = tempContent.replace(/\n{3,}/g, '\n\n');
|
||||||
}
|
newMessage.content = tempContent;
|
||||||
return newMessage;
|
}
|
||||||
});
|
return newMessage;
|
||||||
|
});
|
||||||
return messages;
|
|
||||||
}
|
return messages;
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 将首条消息role置空
|
/**
|
||||||
* @param {Array} messages - 消息数组
|
* roleSet to empty
|
||||||
* @returns {Array} 处理后的消息数组
|
* @param {Array} messages -
|
||||||
*/
|
* @returns {Array}
|
||||||
function clearFirstMessageRole(messages) {
|
*/
|
||||||
if (!Array.isArray(messages) || messages.length === 0) {
|
function clearFirstMessageRole(messages) {
|
||||||
return messages;
|
if (!Array.isArray(messages) || messages.length === 0) {
|
||||||
}
|
return messages;
|
||||||
const processedMessages = messages.map(msg => ({...msg}));
|
}
|
||||||
|
const processedMessages = messages.map(msg => ({...msg}));
|
||||||
processedMessages[0] = {
|
|
||||||
...processedMessages[0],
|
processedMessages[0] = {
|
||||||
role: ''
|
...processedMessages[0],
|
||||||
};
|
role: ''
|
||||||
|
};
|
||||||
return processedMessages;
|
|
||||||
}
|
return processedMessages;
|
||||||
|
}
|
||||||
// 获取角色特征
|
|
||||||
function getRoleFeatures(messages, isClaudeModel, useBackspacePrefix) {
|
// Get role characteristics
|
||||||
let prefix = useBackspacePrefix ? '\u0008' : '';
|
function getRoleFeatures(messages, isClaudeModel, useBackspacePrefix) {
|
||||||
let systemRole = `${prefix}${isClaudeModel ? 'System' : 'system'}`;
|
let prefix = useBackspacePrefix ? '\u0008' : '';
|
||||||
let userRole = `${prefix}${isClaudeModel ? 'Human' : 'user'}`;
|
let systemRole = `${prefix}${isClaudeModel ? 'System' : 'system'}`;
|
||||||
let assistantRole = `${prefix}${isClaudeModel ? 'Assistant' : 'assistant'}`;
|
let userRole = `${prefix}${isClaudeModel ? 'Human' : 'user'}`;
|
||||||
|
let assistantRole = `${prefix}${isClaudeModel ? 'Assistant' : 'assistant'}`;
|
||||||
// 匹配自定义角色
|
|
||||||
const rolePattern = /\[\|(\w+)::(.*?)\|\]/g;
|
// Match custom role
|
||||||
let customRoles = {};
|
const rolePattern = /\[\|(\w+)::(.*?)\|\]/g;
|
||||||
messages.forEach(message => {
|
let customRoles = {};
|
||||||
let content = message.content;
|
messages.forEach(message => {
|
||||||
let match;
|
let content = message.content;
|
||||||
while ((match = rolePattern.exec(content)) !== null) {
|
let match;
|
||||||
const roleKey = match[1]; // 'system', 'user', 'assistant'
|
while ((match = rolePattern.exec(content)) !== null) {
|
||||||
customRoles[roleKey.toLowerCase()] = match[2];
|
const roleKey = match[1]; // 'system', 'user', 'assistant'
|
||||||
}
|
customRoles[roleKey.toLowerCase()] = match[2];
|
||||||
});
|
}
|
||||||
|
});
|
||||||
if (Object.keys(customRoles).length > 0) {
|
|
||||||
prefix = '';
|
if (Object.keys(customRoles).length > 0) {
|
||||||
|
prefix = '';
|
||||||
if (customRoles['system']) {
|
|
||||||
systemRole = customRoles['system'];
|
if (customRoles['system']) {
|
||||||
}
|
systemRole = customRoles['system'];
|
||||||
|
}
|
||||||
if (customRoles['user']) {
|
|
||||||
userRole = customRoles['user'];
|
if (customRoles['user']) {
|
||||||
}
|
userRole = customRoles['user'];
|
||||||
|
}
|
||||||
if (customRoles['assistant']) {
|
|
||||||
assistantRole = customRoles['assistant'];
|
if (customRoles['assistant']) {
|
||||||
}
|
assistantRole = customRoles['assistant'];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return {
|
|
||||||
systemRole,
|
return {
|
||||||
userRole,
|
systemRole,
|
||||||
assistantRole,
|
userRole,
|
||||||
prefix
|
assistantRole,
|
||||||
};
|
prefix
|
||||||
}
|
};
|
||||||
|
}
|
||||||
// 移除 messages 自定义角色格式
|
|
||||||
function removeCustomRoleDefinitions(messages) {
|
// Remove custom role format from messages
|
||||||
const rolePattern = /\[\|\w+::.*?\|]/g;
|
function removeCustomRoleDefinitions(messages) {
|
||||||
|
const rolePattern = /\[\|\w+::.*?\|]/g;
|
||||||
return messages.map(message => {
|
|
||||||
let newContent = message.content.replace(rolePattern, '');
|
return messages.map(message => {
|
||||||
return {
|
let newContent = message.content.replace(rolePattern, '');
|
||||||
...message,
|
return {
|
||||||
content: newContent
|
...message,
|
||||||
};
|
content: newContent
|
||||||
});
|
};
|
||||||
}
|
});
|
||||||
|
}
|
||||||
// 转换角色
|
|
||||||
function convertRoles(messages, roleFeatures) {
|
// Convert role
|
||||||
const {systemRole, userRole, assistantRole} = roleFeatures;
|
function convertRoles(messages, roleFeatures) {
|
||||||
const roleMap = {
|
const {systemRole, userRole, assistantRole} = roleFeatures;
|
||||||
'system': systemRole,
|
const roleMap = {
|
||||||
'user': userRole,
|
'system': systemRole,
|
||||||
'human': userRole,
|
'user': userRole,
|
||||||
'assistant': assistantRole
|
'human': userRole,
|
||||||
};
|
'assistant': assistantRole
|
||||||
|
};
|
||||||
return messages.map(message => {
|
|
||||||
let currentRole = message.role;
|
return messages.map(message => {
|
||||||
|
let currentRole = message.role;
|
||||||
if (currentRole.startsWith('\u0008')) {
|
|
||||||
// 包含前缀不需要转换
|
if (currentRole.startsWith('\u0008')) {
|
||||||
return message;
|
// Contains prefix, no conversion needed
|
||||||
} else {
|
return message;
|
||||||
const roleKey = currentRole.toLowerCase();
|
} else {
|
||||||
const newRole = roleMap[roleKey] || currentRole;
|
const roleKey = currentRole.toLowerCase();
|
||||||
return {...message, role: newRole};
|
const newRole = roleMap[roleKey] || currentRole;
|
||||||
}
|
return {...message, role: newRole};
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
}
|
||||||
// 替换 content 中的角色定义
|
|
||||||
function replaceRolesInContent(messages, roleFeatures) {
|
// Replace role definitions in content
|
||||||
// 避免重复添加
|
function replaceRolesInContent(messages, roleFeatures) {
|
||||||
const roleMap = {
|
// Avoid duplicate addition
|
||||||
'System:': roleFeatures.systemRole.replace(roleFeatures.prefix, '') + ':',
|
const roleMap = {
|
||||||
'system:': roleFeatures.systemRole.replace(roleFeatures.prefix, '') + ':',
|
'System:': roleFeatures.systemRole.replace(roleFeatures.prefix, '') + ':',
|
||||||
'Human:': roleFeatures.userRole.replace(roleFeatures.prefix, '') + ':',
|
'system:': roleFeatures.systemRole.replace(roleFeatures.prefix, '') + ':',
|
||||||
'human:': roleFeatures.userRole.replace(roleFeatures.prefix, '') + ':',
|
'Human:': roleFeatures.userRole.replace(roleFeatures.prefix, '') + ':',
|
||||||
'user:': roleFeatures.userRole.replace(roleFeatures.prefix, '') + ':',
|
'human:': roleFeatures.userRole.replace(roleFeatures.prefix, '') + ':',
|
||||||
'Assistant:': roleFeatures.assistantRole.replace(roleFeatures.prefix, '') + ':',
|
'user:': roleFeatures.userRole.replace(roleFeatures.prefix, '') + ':',
|
||||||
'assistant:': roleFeatures.assistantRole.replace(roleFeatures.prefix, '') + ':',
|
'Assistant:': roleFeatures.assistantRole.replace(roleFeatures.prefix, '') + ':',
|
||||||
};
|
'assistant:': roleFeatures.assistantRole.replace(roleFeatures.prefix, '') + ':',
|
||||||
|
};
|
||||||
// 构建角色正则
|
|
||||||
const escapedLabels = Object.keys(roleMap).map(label => escapeRegExp(label));
|
// Build role regex
|
||||||
const prefixPattern = roleFeatures.prefix ? escapeRegExp(roleFeatures.prefix) : '';
|
const escapedLabels = Object.keys(roleMap).map(label => escapeRegExp(label));
|
||||||
|
const prefixPattern = roleFeatures.prefix ? escapeRegExp(roleFeatures.prefix) : '';
|
||||||
const roleNamesPattern = new RegExp(`(\\n\\n)(${prefixPattern})?(${escapedLabels.join('|')})`, 'g');
|
|
||||||
|
const roleNamesPattern = new RegExp(`(\\n\\n)(${prefixPattern})?(${escapedLabels.join('|')})`, 'g');
|
||||||
return messages.map(message => {
|
|
||||||
let newContent = message.content;
|
return messages.map(message => {
|
||||||
|
let newContent = message.content;
|
||||||
if (typeof newContent === 'string') {
|
|
||||||
// 仅替换段落开头角色
|
if (typeof newContent === 'string') {
|
||||||
newContent = newContent.replace(roleNamesPattern, (match, p1, p2, p3) => {
|
// Only replace role at beginning of paragraph
|
||||||
const newRoleLabel = roleMap[p3] || p3;
|
newContent = newContent.replace(roleNamesPattern, (match, p1, p2, p3) => {
|
||||||
const prefixToUse = p2 !== undefined ? p2 : roleFeatures.prefix;
|
const newRoleLabel = roleMap[p3] || p3;
|
||||||
return p1 + (prefixToUse || '') + newRoleLabel;
|
const prefixToUse = p2 !== undefined ? p2 : roleFeatures.prefix;
|
||||||
});
|
return p1 + (prefixToUse || '') + newRoleLabel;
|
||||||
} else {
|
});
|
||||||
console.warn('message.content is not a string:', newContent);
|
} else {
|
||||||
newContent = '';
|
sysLogger.warn('message.content is not a string:', newContent);
|
||||||
}
|
newContent = '';
|
||||||
|
}
|
||||||
return {
|
|
||||||
...message,
|
return {
|
||||||
content: newContent
|
...message,
|
||||||
};
|
content: newContent
|
||||||
});
|
};
|
||||||
}
|
});
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 在 CLEWD 阶段,对一组 messages 进行二次处理:
|
/**
|
||||||
* 若消息 content 不含 <|KEEP_ROLE|>,则将 role 置空。
|
* CLEWD , messages :
|
||||||
* 然后调用 xmlHyperProcess 做多轮正则、合并、<@N>插入、收尾清理。
|
* content <|KEEP_ROLE|>, role Set to empty。
|
||||||
*
|
* xmlHyperProcess 、、<@N>、Final cleanup。
|
||||||
* @param {Array} messages - [{ role: 'user', content: '...' }, ...]
|
*
|
||||||
* @param {object} roleFeatures - { systemRole, userRole, assistantRole, prefix }
|
* @param {Array} messages - [{ role: 'user', content: '...' }, ...]
|
||||||
* @return {Array} 新的 messages
|
* @param {object} roleFeatures - { systemRole, userRole, assistantRole, prefix }
|
||||||
*/
|
* @return {Array} messages
|
||||||
export function xmlPlotAllMessages(messages, roleFeatures) {
|
*/
|
||||||
return messages.map((msg) => {
|
export function xmlPlotAllMessages(messages, roleFeatures) {
|
||||||
if (!msg.content.includes('<|KEEP_ROLE|>')) {
|
return messages.map((msg) => {
|
||||||
msg = {
|
if (!msg.content.includes('<|KEEP_ROLE|>')) {
|
||||||
...msg,
|
msg = {
|
||||||
role: '' // 置空
|
...msg,
|
||||||
};
|
role: '' // Set to empty
|
||||||
}
|
};
|
||||||
|
}
|
||||||
// 核心调用
|
|
||||||
const processed = xmlHyperProcess(msg.content, roleFeatures);
|
// Core call
|
||||||
|
const processed = xmlHyperProcess(msg.content, roleFeatures);
|
||||||
return { ...msg, content: processed };
|
|
||||||
});
|
return { ...msg, content: processed };
|
||||||
}
|
});
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 多轮 <regex order=1 / 2 / 3>
|
/**
|
||||||
* MergeDisable 判断 + System => user 替换
|
* <regex order=1 / 2 / 3>
|
||||||
* 段落合并 + <@N>插入
|
* MergeDisable + System => user
|
||||||
* 最终收尾 cleanup
|
* + <@N>
|
||||||
*
|
* cleanup
|
||||||
* @param {string} originalContent
|
*
|
||||||
* @param {object} roleFeatures
|
* @param {string} originalContent
|
||||||
* @returns {string} 处理后的文本
|
* @param {object} roleFeatures
|
||||||
*/
|
* @returns {string}
|
||||||
function xmlHyperProcess(originalContent, roleFeatures) {
|
*/
|
||||||
let content = originalContent;
|
function xmlHyperProcess(originalContent, roleFeatures) {
|
||||||
let regexLogs = '';
|
let content = originalContent;
|
||||||
|
let regexLogs = '';
|
||||||
// 第1轮正则 (order=1)
|
|
||||||
[content, regexLogs] = xmlHyperRegex(content, 1, regexLogs);
|
// 1st round regex (order=1)
|
||||||
|
[content, regexLogs] = xmlHyperRegex(content, 1, regexLogs);
|
||||||
// 检测 MergeDisable 标记
|
|
||||||
const mergeDisable = {
|
// Detect MergeDisable tag
|
||||||
all: content.includes('<|Merge Disable|>'),
|
const mergeDisable = {
|
||||||
system: content.includes('<|Merge System Disable|>'),
|
all: content.includes('<|Merge Disable|>'),
|
||||||
user: content.includes('<|Merge Human Disable|>'),
|
system: content.includes('<|Merge System Disable|>'),
|
||||||
assistant: content.includes('<|Merge Assistant Disable|>')
|
user: content.includes('<|Merge Human Disable|>'),
|
||||||
};
|
assistant: content.includes('<|Merge Assistant Disable|>')
|
||||||
|
};
|
||||||
// 把“system:”在部分情况下替换为“user:”
|
|
||||||
content = preSystemToUserFallback(content, roleFeatures, mergeDisable);
|
// Replace 'system:' with 'user:' in some cases
|
||||||
|
content = preSystemToUserFallback(content, roleFeatures, mergeDisable);
|
||||||
// 开始合并 (首次)
|
|
||||||
content = xmlHyperMerge(content, roleFeatures, mergeDisable);
|
// Start merging (first time)
|
||||||
|
content = xmlHyperMerge(content, roleFeatures, mergeDisable);
|
||||||
// 处理 <@N> 插入
|
|
||||||
content = handleSubInsertion(content);
|
// Process <@N> insertion
|
||||||
|
content = handleSubInsertion(content);
|
||||||
// 第2轮正则 (order=2)
|
|
||||||
[content, regexLogs] = xmlHyperRegex(content, 2, regexLogs);
|
// 2nd round regex (order=2)
|
||||||
|
[content, regexLogs] = xmlHyperRegex(content, 2, regexLogs);
|
||||||
// 第2次合并
|
|
||||||
content = xmlHyperMerge(content, roleFeatures, mergeDisable);
|
// 2nd merge
|
||||||
|
content = xmlHyperMerge(content, roleFeatures, mergeDisable);
|
||||||
// 第3轮正则 (order=3)
|
|
||||||
[content, regexLogs] = xmlHyperRegex(content, 3, regexLogs);
|
// 3rd round regex (order=3)
|
||||||
|
[content, regexLogs] = xmlHyperRegex(content, 3, regexLogs);
|
||||||
// 插入对 <|padtxtX|> 的处理 or countTokens, etc.
|
|
||||||
|
// Insert handling for <|padtxtX|> or countTokens, etc.
|
||||||
// 收尾清理
|
|
||||||
content = finalizeCleanup(content);
|
// Final cleanup
|
||||||
|
content = finalizeCleanup(content);
|
||||||
return content.trim();
|
|
||||||
}
|
return content.trim();
|
||||||
|
}
|
||||||
/**
|
|
||||||
* xmlHyperRegex:
|
/**
|
||||||
* 匹配 <regex order=?> " /pattern/flags ":" replacement" </regex> 并完成替换
|
* xmlHyperRegex:
|
||||||
*/
|
* <regex order=?> " /pattern/flags ":" replacement" </regex>
|
||||||
function xmlHyperRegex(original, order, logs) {
|
*/
|
||||||
let content = original;
|
function xmlHyperRegex(original, order, logs) {
|
||||||
// 仅处理同 order 的 block
|
let content = original;
|
||||||
const patternRegex = new RegExp(
|
// Only process blocks with same order
|
||||||
`<regex(?: +order *= *(${order}))?>\\s*"\\/([^"]*?)\\/([gimsyu]*)"\\s*:\\s*"(.*?)"\\s*<\\/regex>`,
|
const patternRegex = new RegExp(
|
||||||
'gm'
|
`<regex(?: +order *= *(${order}))?>\\s*"\\/([^"]*?)\\/([gimsyu]*)"\\s*:\\s*"(.*?)"\\s*<\\/regex>`,
|
||||||
);
|
'gm'
|
||||||
|
);
|
||||||
let match;
|
|
||||||
while ((match = patternRegex.exec(content)) !== null) {
|
let match;
|
||||||
const entire = match[0];
|
while ((match = patternRegex.exec(content)) !== null) {
|
||||||
const rawPattern = match[2];
|
const entire = match[0];
|
||||||
const rawFlags = match[3];
|
const rawPattern = match[2];
|
||||||
let replacement = match[4];
|
const rawFlags = match[3];
|
||||||
|
let replacement = match[4];
|
||||||
logs += `${entire}\n`;
|
|
||||||
try {
|
logs += `${entire}\n`;
|
||||||
const regObj = new RegExp(rawPattern, rawFlags);
|
try {
|
||||||
replacement = JSON.parse(`"${replacement.replace(/\\?"/g, '\\"')}"`); // 反序列化
|
const regObj = new RegExp(rawPattern, rawFlags);
|
||||||
content = content.replace(regObj, replacement);
|
replacement = JSON.parse(`"${replacement.replace(/\\?"/g, '\\"')}"`); // Deserialize
|
||||||
} catch (err) {
|
content = content.replace(regObj, replacement);
|
||||||
console.warn(`Regex parse/replace error in block: ${entire}\n`, err);
|
} catch (err) {
|
||||||
}
|
sysLogger.warn(`Regex parse/replace error in block: ${entire}\n`, err);
|
||||||
}
|
}
|
||||||
return [content, logs];
|
}
|
||||||
}
|
return [content, logs];
|
||||||
|
}
|
||||||
/**
|
|
||||||
* content = content.replace(...)
|
/**
|
||||||
* 而后 system: => user:
|
* content = content.replace(...)
|
||||||
* 根据 roleFeatures.systemRole / userRole 动态替换
|
* system: => user:
|
||||||
*/
|
* roleFeatures.systemRole / userRole
|
||||||
function preSystemToUserFallback(content, roleFeatures, mergeDisable) {
|
*/
|
||||||
if (mergeDisable.all || mergeDisable.system || mergeDisable.user) {
|
function preSystemToUserFallback(content, roleFeatures, mergeDisable) {
|
||||||
return content; // 不做任何替换
|
if (mergeDisable.all || mergeDisable.system || mergeDisable.user) {
|
||||||
}
|
return content; // Do not replace
|
||||||
|
}
|
||||||
const systemName = stripPrefix(roleFeatures.systemRole, roleFeatures.prefix);
|
|
||||||
const userName = stripPrefix(roleFeatures.userRole, roleFeatures.prefix);
|
const systemName = stripPrefix(roleFeatures.systemRole, roleFeatures.prefix);
|
||||||
const assistantName = stripPrefix(roleFeatures.assistantRole, roleFeatures.prefix);
|
const userName = stripPrefix(roleFeatures.userRole, roleFeatures.prefix);
|
||||||
|
const assistantName = stripPrefix(roleFeatures.assistantRole, roleFeatures.prefix);
|
||||||
// 前面非 user|assistant 段落时的 system: -> 去掉
|
|
||||||
const re1 = new RegExp(
|
// Remove system: prefix when preceding non-user|assistant paragraphs
|
||||||
`(\\n\\n|^\\s*)(?<!\\n\\n(${userName}|${assistantName}):.*?)${systemName}:\\s*`,
|
const re1 = new RegExp(
|
||||||
'gs'
|
`(\\n\\n|^\\s*)(?<!\\n\\n(${userName}|${assistantName}):.*?)${systemName}:\\s*`,
|
||||||
);
|
'gs'
|
||||||
content = content.replace(re1, '$1');
|
);
|
||||||
|
content = content.replace(re1, '$1');
|
||||||
// 补充 system => user
|
|
||||||
const re2 = new RegExp(`(\\n\\n|^\\s*)${systemName}:\\s*`, 'g');
|
// Supplement system => user
|
||||||
content = content.replace(re2, `\n\n${userName}: `);
|
const re2 = new RegExp(`(\\n\\n|^\\s*)${systemName}:\\s*`, 'g');
|
||||||
|
content = content.replace(re2, `\n\n${userName}: `);
|
||||||
return content;
|
|
||||||
}
|
return content;
|
||||||
|
}
|
||||||
/**
|
|
||||||
* xmlHyperMerge:
|
/**
|
||||||
* 利用“段落合并”逻辑,动态 roleFeatures
|
* xmlHyperMerge:
|
||||||
*/
|
* “”, roleFeatures
|
||||||
function xmlHyperMerge(original, roleFeatures, mergeDisable) {
|
*/
|
||||||
let content = original;
|
function xmlHyperMerge(original, roleFeatures, mergeDisable) {
|
||||||
if (mergeDisable.all) {
|
let content = original;
|
||||||
return content; // 禁用合并
|
if (mergeDisable.all) {
|
||||||
}
|
return content; // Disable merge
|
||||||
// 先获取名称
|
}
|
||||||
const sys = stripPrefix(roleFeatures.systemRole, roleFeatures.prefix);
|
// First get name
|
||||||
const usr = stripPrefix(roleFeatures.userRole, roleFeatures.prefix);
|
const sys = stripPrefix(roleFeatures.systemRole, roleFeatures.prefix);
|
||||||
const ast = stripPrefix(roleFeatures.assistantRole, roleFeatures.prefix);
|
const usr = stripPrefix(roleFeatures.userRole, roleFeatures.prefix);
|
||||||
|
const ast = stripPrefix(roleFeatures.assistantRole, roleFeatures.prefix);
|
||||||
// 合并 system 段
|
|
||||||
if (!mergeDisable.system) {
|
// Merge system section
|
||||||
const regSys = new RegExp(`(?:\\n\\n|^\\s*)${escapeRegExp(sys)}:\\s*(.*?)(?=\\n\\n(?:${escapeRegExp(usr)}|${escapeRegExp(ast)}|$))`, 'gs');
|
if (!mergeDisable.system) {
|
||||||
content = content.replace(regSys, (_m, p1) => `\n\n${sys}: ${p1}`);
|
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 段
|
|
||||||
if (!mergeDisable.user) {
|
// Merge user section
|
||||||
const regUsr = new RegExp(`(?:\\n\\n|^\\s*)${escapeRegExp(usr)}:\\s*(.*?)(?=\\n\\n(?:${escapeRegExp(ast)}|${escapeRegExp(sys)}|$))`, 'gs');
|
if (!mergeDisable.user) {
|
||||||
content = content.replace(regUsr, (_m, p1) => `\n\n${usr}: ${p1}`);
|
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 段
|
|
||||||
if (!mergeDisable.assistant) {
|
// Merge assistant section
|
||||||
const regAst = new RegExp(`(?:\\n\\n|^\\s*)${escapeRegExp(ast)}:\\s*(.*?)(?=\\n\\n(?:${escapeRegExp(usr)}|${escapeRegExp(sys)}|$))`, 'gs');
|
if (!mergeDisable.assistant) {
|
||||||
content = content.replace(regAst, (_m, p1) => `\n\n${ast}: ${p1}`);
|
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}`);
|
||||||
|
}
|
||||||
return content;
|
|
||||||
}
|
return content;
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 先 splitContent = content.split(regExp)
|
/**
|
||||||
* 然后 for each <@N> => 把里面内容插入到 “倒数第N段落”后面
|
* splitContent = content.split(regExp)
|
||||||
* 删除 match[0]
|
* for each <@N> => “N”
|
||||||
*/
|
* match[0]
|
||||||
function handleSubInsertion(original) {
|
*/
|
||||||
let content = original;
|
function handleSubInsertion(original) {
|
||||||
|
let content = original;
|
||||||
// 按 \n\n(?=.*?:) 拆分
|
|
||||||
let splitted = content.split(/\n\n(?=.*?:)/g);
|
// Split by \n\n(?=.*?:)
|
||||||
|
let splitted = content.split(/\n\n(?=.*?:)/g);
|
||||||
let match;
|
|
||||||
while ((match = /<@(\d+)>(.*?)<\/@\1>/gs.exec(content)) !== null) {
|
let match;
|
||||||
const idx = splitted.length - parseInt(match[1], 10) - 1;
|
while ((match = /<@(\d+)>(.*?)<\/@\1>/gs.exec(content)) !== null) {
|
||||||
if (idx >= 0 && splitted[idx]) {
|
const idx = splitted.length - parseInt(match[1], 10) - 1;
|
||||||
splitted[idx] += `\n\n${match[2]}`;
|
if (idx >= 0 && splitted[idx]) {
|
||||||
}
|
splitted[idx] += `\n\n${match[2]}`;
|
||||||
content = content.replace(match[0], '');
|
}
|
||||||
}
|
content = content.replace(match[0], '');
|
||||||
|
}
|
||||||
// 重组
|
|
||||||
content = splitted.join('\n\n').replace(/<@(\d+)>.*?<\/@\1>/gs, '');
|
// Reassemble
|
||||||
return content;
|
content = splitted.join('\n\n').replace(/<@(\d+)>.*?<\/@\1>/gs, '');
|
||||||
}
|
return content;
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 移除 <regex> 块、统一换行、<|curtail|> => 换行,<|join|> => 空
|
/**
|
||||||
* <|space|> => ' ',以及 <|xxx|> => JSON.parse
|
* Remove <regex> block、Unify line breaks、<|curtail|> => newline,<|join|> =>
|
||||||
* 然后去除多余空行
|
* <|space|> => ' ', <|xxx|> => JSON.parse
|
||||||
*/
|
* Remove extra blank lines
|
||||||
function finalizeCleanup(original) {
|
*/
|
||||||
let content = original;
|
function finalizeCleanup(original) {
|
||||||
// 移除 <regex> 块
|
let content = original;
|
||||||
content = content.replace(/<regex( +order *= *\d)?>.*?<\/regex>/gm, '');
|
// Remove <regex> block
|
||||||
|
content = content.replace(/<regex( +order *= *\d)?>.*?<\/regex>/gm, '');
|
||||||
// 统一换行
|
|
||||||
content = content.replace(/\r\n|\r/gm, '\n');
|
// Unify line breaks
|
||||||
|
content = content.replace(/\r\n|\r/gm, '\n');
|
||||||
// <|curtail|> => 换行
|
|
||||||
content = content.replace(/\s*<\|curtail\|>\s*/g, '\n');
|
// <|curtail|> => newline
|
||||||
|
content = content.replace(/\s*<\|curtail\|>\s*/g, '\n');
|
||||||
// <|join|> => ''
|
|
||||||
content = content.replace(/\s*<\|join\|>\s*/g, '');
|
// <|join|> => ''
|
||||||
|
content = content.replace(/\s*<\|join\|>\s*/g, '');
|
||||||
// <|space|> => ' '
|
|
||||||
content = content.replace(/\s*<\|space\|>\s*/g, ' ');
|
// <|space|> => ' '
|
||||||
|
content = content.replace(/\s*<\|space\|>\s*/g, ' ');
|
||||||
// JSON反序列化
|
|
||||||
content = content.replace(/<\|(\\.*?)\|>/g, (m, p1) => {
|
// JSON deserialization
|
||||||
try {
|
content = content.replace(/<\|(\\.*?)\|>/g, (m, p1) => {
|
||||||
return JSON.parse(`"${p1.replace(/\\?"/g, '\\"')}"`);
|
try {
|
||||||
} catch {
|
return JSON.parse(`"${p1.replace(/\\?"/g, '\\"')}"`);
|
||||||
return m; // 保留原文本
|
} catch {
|
||||||
}
|
return m; // Keep original text
|
||||||
});
|
}
|
||||||
|
});
|
||||||
// 移除其他 <|xxx|> 标记
|
|
||||||
content = content.replace(/\s*<\|(?!padtxt).*?\|>\s*/g, '\n\n');
|
// Remove other <|xxx|> tags
|
||||||
|
content = content.replace(/\s*<\|(?!padtxt).*?\|>\s*/g, '\n\n');
|
||||||
// 去除多余空行
|
|
||||||
content = content.trim().replace(/(?<=\n)\n(?=\n)/g, '');
|
// Remove extra blank lines
|
||||||
|
content = content.trim().replace(/(?<=\n)\n(?=\n)/g, '');
|
||||||
return content;
|
|
||||||
}
|
return content;
|
||||||
|
}
|
||||||
/** stripPrefix: 如果 role 中含有前缀(如 \u0008),去掉 */
|
|
||||||
function stripPrefix(fullStr, prefix) {
|
/** stripPrefix: role ( \u0008), */
|
||||||
if (!prefix) return fullStr;
|
function stripPrefix(fullStr, prefix) {
|
||||||
if (fullStr.startsWith(prefix)) {
|
if (!prefix) return fullStr;
|
||||||
return fullStr.slice(prefix.length);
|
if (fullStr.startsWith(prefix)) {
|
||||||
}
|
return fullStr.slice(prefix.length);
|
||||||
return fullStr;
|
}
|
||||||
}
|
return fullStr;
|
||||||
|
}
|
||||||
/** 转义正则特殊字符 */
|
|
||||||
function escapeRegExp(str) {
|
/** */
|
||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
function escapeRegExp(str) {
|
||||||
}
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
@@ -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)}...`);
|
||||||
|
});
|
||||||
|
}
|
||||||
+1031
-926
File diff suppressed because it is too large
Load Diff
@@ -1,51 +1,52 @@
|
|||||||
import dns from 'dns';
|
import dns from 'dns';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import sysLogger from './utils/sysLogger.mjs';
|
||||||
class NetworkMonitor extends EventEmitter {
|
|
||||||
constructor() {
|
class NetworkMonitor extends EventEmitter {
|
||||||
super();
|
constructor() {
|
||||||
this.isBlocked = false;
|
super();
|
||||||
this.checkInterval = null;
|
this.isBlocked = false;
|
||||||
}
|
this.checkInterval = null;
|
||||||
|
}
|
||||||
async checkConnection() {
|
|
||||||
return new Promise((resolve) => {
|
async checkConnection() {
|
||||||
dns.lookup('you.com', (err) => {
|
return new Promise((resolve) => {
|
||||||
if (err && err.code === 'ENOTFOUND') {
|
dns.lookup('you.com', (err) => {
|
||||||
resolve(false);
|
if (err && err.code === 'ENOTFOUND') {
|
||||||
} else {
|
resolve(false);
|
||||||
resolve(true);
|
} else {
|
||||||
}
|
resolve(true);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
}
|
||||||
async startMonitoring() {
|
|
||||||
console.log("开始网络监控...");
|
async startMonitoring() {
|
||||||
this.checkInterval = setInterval(async () => {
|
sysLogger.debug("Starting network monitor...");
|
||||||
const isConnected = await this.checkConnection();
|
this.checkInterval = setInterval(async () => {
|
||||||
if (!isConnected && !this.isBlocked) {
|
const isConnected = await this.checkConnection();
|
||||||
this.isBlocked = true;
|
if (!isConnected && !this.isBlocked) {
|
||||||
this.emit('networkDown');
|
this.isBlocked = true;
|
||||||
console.log("检测到网络异常");
|
this.emit('networkDown');
|
||||||
} else if (isConnected && this.isBlocked) {
|
sysLogger.debug("Network anomaly detected");
|
||||||
this.isBlocked = false;
|
} else if (isConnected && this.isBlocked) {
|
||||||
this.emit('networkUp');
|
this.isBlocked = false;
|
||||||
console.log("网络恢复正常");
|
this.emit('networkUp');
|
||||||
}
|
sysLogger.debug("Network recovered");
|
||||||
}, 5000);
|
}
|
||||||
}
|
}, 5000);
|
||||||
|
}
|
||||||
stopMonitoring() {
|
|
||||||
if (this.checkInterval) {
|
stopMonitoring() {
|
||||||
clearInterval(this.checkInterval);
|
if (this.checkInterval) {
|
||||||
this.checkInterval = null;
|
clearInterval(this.checkInterval);
|
||||||
}
|
this.checkInterval = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
isNetworkBlocked() {
|
|
||||||
return this.isBlocked;
|
isNetworkBlocked() {
|
||||||
}
|
return this.isBlocked;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default NetworkMonitor;
|
export default NetworkMonitor;
|
||||||
+112
-111
@@ -1,111 +1,112 @@
|
|||||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||||
import HttpsProxyAgent from 'https-proxy-agent';
|
import HttpsProxyAgent from 'https-proxy-agent';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
|
import sysLogger from './utils/sysLogger.mjs';
|
||||||
let globalProxyAgent = null;
|
|
||||||
|
let globalProxyAgent = null;
|
||||||
function getProxyUrl() {
|
|
||||||
const proxyUrl = process.env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY;
|
function getProxyUrl() {
|
||||||
return proxyUrl ? proxyUrl.trim() : null;
|
const proxyUrl = process.env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY;
|
||||||
}
|
return proxyUrl ? proxyUrl.trim() : null;
|
||||||
|
}
|
||||||
function parseProxyUrl(proxyUrl) {
|
|
||||||
if (!proxyUrl) return null;
|
function parseProxyUrl(proxyUrl) {
|
||||||
|
if (!proxyUrl) return null;
|
||||||
try {
|
|
||||||
let protocol, host, port, username, password;
|
try {
|
||||||
if (proxyUrl.startsWith('socks5://')) {
|
let protocol, host, port, username, password;
|
||||||
const parts = proxyUrl.slice(9).split(':');
|
if (proxyUrl.startsWith('socks5://')) {
|
||||||
if (parts.length === 4) {
|
const parts = proxyUrl.slice(9).split(':');
|
||||||
[host, port, username, password] = parts;
|
if (parts.length === 4) {
|
||||||
protocol = 'socks5:';
|
[host, port, username, password] = parts;
|
||||||
} else {
|
protocol = 'socks5:';
|
||||||
throw new Error('Invalid SOCKS5 proxy URL format');
|
} else {
|
||||||
}
|
throw new Error('Invalid SOCKS5 proxy URL format');
|
||||||
} else {
|
}
|
||||||
const url = new URL(proxyUrl);
|
} else {
|
||||||
protocol = url.protocol;
|
const url = new URL(proxyUrl);
|
||||||
host = url.hostname;
|
protocol = url.protocol;
|
||||||
port = url.port;
|
host = url.hostname;
|
||||||
username = url.username;
|
port = url.port;
|
||||||
password = url.password;
|
username = url.username;
|
||||||
}
|
password = url.password;
|
||||||
|
}
|
||||||
return { protocol: protocol.replace(':', ''), host, port, username, password };
|
|
||||||
} catch (error) {
|
return { protocol: protocol.replace(':', ''), host, port, username, password };
|
||||||
console.error(`Invalid proxy URL: ${proxyUrl}`);
|
} catch (error) {
|
||||||
return null;
|
sysLogger.error(`Invalid proxy URL: ${proxyUrl}`);
|
||||||
}
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function createProxyAgent() {
|
|
||||||
const proxyUrl = getProxyUrl();
|
function createProxyAgent() {
|
||||||
if (!proxyUrl) {
|
const proxyUrl = getProxyUrl();
|
||||||
console.log('Proxy environment variable not set, will not use proxy.');
|
if (!proxyUrl) {
|
||||||
return null;
|
sysLogger.info('Proxy environment variable not set, will not use proxy.');
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
const parsedProxy = parseProxyUrl(proxyUrl);
|
|
||||||
if (!parsedProxy) 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');
|
if (parsedProxy.protocol === 'socks5') {
|
||||||
return new SocksProxyAgent({
|
sysLogger.info('Using SOCKS5 proxy');
|
||||||
hostname: parsedProxy.host,
|
return new SocksProxyAgent({
|
||||||
port: parsedProxy.port,
|
hostname: parsedProxy.host,
|
||||||
userId: parsedProxy.username,
|
port: parsedProxy.port,
|
||||||
password: parsedProxy.password,
|
userId: parsedProxy.username,
|
||||||
protocol: 'socks5:'
|
password: parsedProxy.password,
|
||||||
});
|
protocol: 'socks5:'
|
||||||
} else {
|
});
|
||||||
console.log('Using HTTP/HTTPS proxy');
|
} else {
|
||||||
return new HttpsProxyAgent.HttpsProxyAgent(proxyUrl);
|
sysLogger.info('Using HTTP/HTTPS proxy');
|
||||||
}
|
return new HttpsProxyAgent.HttpsProxyAgent(proxyUrl);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
export function setGlobalProxy() {
|
|
||||||
const proxyUrl = getProxyUrl();
|
export function setGlobalProxy() {
|
||||||
if (proxyUrl) {
|
const proxyUrl = getProxyUrl();
|
||||||
globalProxyAgent = createProxyAgent();
|
if (proxyUrl) {
|
||||||
|
globalProxyAgent = createProxyAgent();
|
||||||
// 重写 http 和 https 模块的 request 方法
|
|
||||||
const originalHttpRequest = http.request;
|
// Rewrite request method of http and https modules
|
||||||
const originalHttpsRequest = https.request;
|
const originalHttpRequest = http.request;
|
||||||
|
const originalHttpsRequest = https.request;
|
||||||
http.request = function(options, callback) {
|
|
||||||
if (typeof options === 'string') {
|
http.request = function(options, callback) {
|
||||||
options = new URL(options);
|
if (typeof options === 'string') {
|
||||||
}
|
options = new URL(options);
|
||||||
options.agent = globalProxyAgent;
|
}
|
||||||
return originalHttpRequest.call(this, options, callback);
|
options.agent = globalProxyAgent;
|
||||||
};
|
return originalHttpRequest.call(this, options, callback);
|
||||||
|
};
|
||||||
https.request = function(options, callback) {
|
|
||||||
if (typeof options === 'string') {
|
https.request = function(options, callback) {
|
||||||
options = new URL(options);
|
if (typeof options === 'string') {
|
||||||
}
|
options = new URL(options);
|
||||||
options.agent = globalProxyAgent;
|
}
|
||||||
return originalHttpsRequest.call(this, options, callback);
|
options.agent = globalProxyAgent;
|
||||||
};
|
return originalHttpsRequest.call(this, options, callback);
|
||||||
|
};
|
||||||
console.log(`Global proxy set to: ${proxyUrl}`);
|
|
||||||
} else {
|
sysLogger.info(`Global proxy set to: ${proxyUrl}`);
|
||||||
console.log('No proxy environment variable set, global proxy not configured.');
|
} else {
|
||||||
}
|
sysLogger.info('No proxy environment variable set, global proxy not configured.');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
export function setProxyEnvironmentVariables() {
|
|
||||||
const proxyUrl = getProxyUrl();
|
export function setProxyEnvironmentVariables() {
|
||||||
if (proxyUrl) {
|
const proxyUrl = getProxyUrl();
|
||||||
process.env.HTTP_PROXY = proxyUrl;
|
if (proxyUrl) {
|
||||||
process.env.HTTPS_PROXY = proxyUrl;
|
process.env.HTTP_PROXY = proxyUrl;
|
||||||
console.log(`Set proxy environment variables to: ${proxyUrl}`);
|
process.env.HTTPS_PROXY = proxyUrl;
|
||||||
}
|
sysLogger.info(`Set proxy environment variables to: ${proxyUrl}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 全局代理
|
|
||||||
setGlobalProxy();
|
// Global proxy
|
||||||
|
setGlobalProxy();
|
||||||
@@ -1,86 +1,65 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {Mutex} from 'async-mutex';
|
import {Mutex} from 'async-mutex';
|
||||||
import winston from 'winston';
|
import sysLogger from './utils/sysLogger.mjs';
|
||||||
|
// Logging
|
||||||
// 请求日志记录
|
const logFilePath = path.join(process.cwd(), 'data', 'request_logs.log');
|
||||||
const logFilePath = path.join(process.cwd(), 'request_logs.log');
|
const blacklistFilePath = path.join(process.cwd(), 'data', 'ip_blacklist.json');
|
||||||
|
const tempLimitFilePath = path.join(process.cwd(), 'data', 'temp_limits.json');
|
||||||
/*
|
|
||||||
* 黑名单文件格式:
|
|
||||||
* {
|
|
||||||
* "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');
|
|
||||||
|
|
||||||
// 是否启用检测
|
|
||||||
const ENABLE_DETECTION = process.env.ENABLE_DETECTION !== 'false';
|
const ENABLE_DETECTION = process.env.ENABLE_DETECTION !== 'false';
|
||||||
|
|
||||||
class RequestLogger {
|
class RequestLogger {
|
||||||
constructor() {
|
constructor() {
|
||||||
// 日志记录
|
// Logging
|
||||||
this.logger = winston.createLogger({
|
this.logger = {
|
||||||
level: 'info',
|
info: async (message) => {
|
||||||
format: winston.format.combine(
|
const timestamp = new Date().toLocaleString('zh-CN', {
|
||||||
winston.format.timestamp({format: 'YYYY年MM月DD日 HH时mm分ss秒'}),
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
winston.format.printf(({timestamp, level, message}) => {
|
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
|
||||||
return `${timestamp} | ${message}`;
|
}).replace(/\//g, '-');
|
||||||
})
|
const logLine = `${timestamp} | ${message}\n`;
|
||||||
),
|
|
||||||
transports: [
|
try {
|
||||||
new winston.transports.File({filename: logFilePath})
|
await fs.promises.appendFile(logFilePath, logLine);
|
||||||
]
|
} catch (e) {
|
||||||
});
|
sysLogger.error('Failed to write to request log:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(blacklistFilePath)) {
|
if (!fs.existsSync(blacklistFilePath)) {
|
||||||
fs.writeFileSync(blacklistFilePath, JSON.stringify({}), 'utf8');
|
fs.writeFileSync(blacklistFilePath, JSON.stringify({}), 'utf8');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`初始化黑名单文件出错:${err.message}`);
|
sysLogger.error(`Error initializing blacklist file: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ipRequestRecords = {}; // 每个IP的请求时间记录
|
this.ipRequestRecords = {}; // Request time record per IP
|
||||||
this.ip30sTriggerCount = {}; // 统计每个IP触发30秒限制的次数
|
this.ip30sTriggerCount = {}; // Count 30s limit triggers for each IP
|
||||||
this.temporaryLimits = {}; // 每个IP的临时限制到期时间
|
this.temporaryLimits = {}; // Temp limit expiration time per IP
|
||||||
this.ipMutexes = {}; // 为每个IP分配一个独立的Mutex
|
this.ipMutexes = {}; // Assign an independent Mutex for each IP
|
||||||
this.blacklistMutex = new Mutex(); // 黑名单文件的Mutex
|
this.blacklistMutex = new Mutex(); // Blacklist file Mutex
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(tempLimitFilePath)) {
|
if (!fs.existsSync(tempLimitFilePath)) {
|
||||||
fs.writeFileSync(tempLimitFilePath, JSON.stringify({}), 'utf8');
|
fs.writeFileSync(tempLimitFilePath, JSON.stringify({}), 'utf8');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`初始化临时限制文件出错:${err.message}`);
|
sysLogger.error(`Error initializing temp limit file: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载临时限制
|
// Load temp limits
|
||||||
this.loadTempLimitsFromFile();
|
this.loadTempLimitsFromFile();
|
||||||
|
|
||||||
// 监听文件变化
|
// Listen to file changes
|
||||||
this.setupTempLimitsFileWatcher();
|
this.setupTempLimitsFileWatcher();
|
||||||
|
|
||||||
// 清理无活动IP
|
// Clean inactive IPs
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
this.cleanUpInactiveIPs();
|
this.cleanUpInactiveIPs();
|
||||||
}, 60 * 60 * 1000);
|
}, 60 * 60 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取IP的Mutex
|
// Get IP Mutex
|
||||||
getMutexForIP(ip) {
|
getMutexForIP(ip) {
|
||||||
if (!this.ipMutexes[ip]) {
|
if (!this.ipMutexes[ip]) {
|
||||||
this.ipMutexes[ip] = new Mutex();
|
this.ipMutexes[ip] = new Mutex();
|
||||||
@@ -88,17 +67,17 @@ class RequestLogger {
|
|||||||
return this.ipMutexes[ip];
|
return this.ipMutexes[ip];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从文件加载临时限制
|
// Load temporary limits from file
|
||||||
loadTempLimitsFromFile() {
|
async loadTempLimitsFromFile() {
|
||||||
try {
|
try {
|
||||||
const dataRaw = fs.readFileSync(tempLimitFilePath, 'utf8');
|
const dataRaw = await fs.promises.readFile(tempLimitFilePath, 'utf8');
|
||||||
if (!dataRaw.trim()) {
|
if (!dataRaw.trim()) {
|
||||||
// 文件为空,则 no-op
|
// File is empty, no-op
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parsed = JSON.parse(dataRaw);
|
const parsed = JSON.parse(dataRaw);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// 清空内存
|
// Clear memory
|
||||||
this.temporaryLimits = {};
|
this.temporaryLimits = {};
|
||||||
for (const ip in parsed) {
|
for (const ip in parsed) {
|
||||||
const expireTime = parseInt(parsed[ip], 10);
|
const expireTime = parseInt(parsed[ip], 10);
|
||||||
@@ -106,28 +85,28 @@ class RequestLogger {
|
|||||||
this.temporaryLimits[ip] = expireTime;
|
this.temporaryLimits[ip] = expireTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.saveTempLimitsToFile();
|
await this.saveTempLimitsToFile();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`读取临时限制文件出错:${error.message}`);
|
sysLogger.error(`Error reading temp limit file: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveTempLimitsToFile() {
|
async saveTempLimitsToFile() {
|
||||||
try {
|
try {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// 移除过期记录
|
// Remove expired records
|
||||||
for (const ip in this.temporaryLimits) {
|
for (const ip in this.temporaryLimits) {
|
||||||
if (this.temporaryLimits[ip] <= now) {
|
if (this.temporaryLimits[ip] <= now) {
|
||||||
delete this.temporaryLimits[ip];
|
delete this.temporaryLimits[ip];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fs.writeFileSync(
|
await fs.promises.writeFile(
|
||||||
tempLimitFilePath,
|
tempLimitFilePath,
|
||||||
JSON.stringify(this.temporaryLimits, null, 4),
|
JSON.stringify(this.temporaryLimits, null, 4),
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`写入临时限制文件出错:${error.message}`);
|
sysLogger.error(`Error writing temp limit file: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,61 +118,65 @@ class RequestLogger {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`监听临时限制文件出错:${err.message}`);
|
sysLogger.error(`Error listening to temp limit file: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否在黑名单
|
// Check if in blacklist
|
||||||
async isBlacklisted(ip) {
|
async isBlacklisted(ip) {
|
||||||
return await this.blacklistMutex.runExclusive(async () => {
|
return await this.blacklistMutex.runExclusive(async () => {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(blacklistFilePath)) {
|
try {
|
||||||
fs.writeFileSync(blacklistFilePath, JSON.stringify({}), 'utf8');
|
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 blacklist = JSON.parse(blacklistData || '{}');
|
||||||
const info = blacklist[ip];
|
const info = blacklist[ip];
|
||||||
if (info && info.permanent) {
|
if (info && info.permanent) {
|
||||||
return true;
|
return true;
|
||||||
} else if (info) {
|
} else if (info) {
|
||||||
// 移除非法
|
// Remove invalid
|
||||||
delete blacklist[ip];
|
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;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`读取黑名单文件出错:${err.message}`);
|
sysLogger.error(`Error reading blacklist file: ${err.message}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将IP添加到黑名单
|
// Add IP to blacklist
|
||||||
async addToBlacklist(ip) {
|
async addToBlacklist(ip) {
|
||||||
await this.blacklistMutex.runExclusive(async () => {
|
await this.blacklistMutex.runExclusive(async () => {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(blacklistFilePath)) {
|
try {
|
||||||
fs.writeFileSync(blacklistFilePath, JSON.stringify({}), 'utf8');
|
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 || '{}');
|
const blacklist = JSON.parse(dataRaw || '{}');
|
||||||
|
|
||||||
if (!blacklist[ip]) {
|
if (!blacklist[ip]) {
|
||||||
blacklist[ip] = {permanent: true};
|
blacklist[ip] = {permanent: true};
|
||||||
fs.writeFileSync(
|
await fs.promises.writeFile(
|
||||||
blacklistFilePath,
|
blacklistFilePath,
|
||||||
JSON.stringify(blacklist, null, 4),
|
JSON.stringify(blacklist, null, 4),
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`写入黑名单文件出错:${err.message}`);
|
sysLogger.error(`Error writing blacklist file: ${err.message}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否有临时限制
|
// Check for temp limits
|
||||||
_hasTemporaryLimit(ip) {
|
_hasTemporaryLimit(ip) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (this.temporaryLimits[ip] && now < this.temporaryLimits[ip]) {
|
if (this.temporaryLimits[ip] && now < this.temporaryLimits[ip]) {
|
||||||
@@ -203,7 +186,7 @@ class RequestLogger {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取剩余限制时间
|
// Get remaining limit time
|
||||||
_getRemainingTime(ip) {
|
_getRemainingTime(ip) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (this.temporaryLimits[ip] && now < this.temporaryLimits[ip]) {
|
if (this.temporaryLimits[ip] && now < this.temporaryLimits[ip]) {
|
||||||
@@ -212,45 +195,45 @@ class RequestLogger {
|
|||||||
const mm = Math.floor((remaining % 3600000) / 60000);
|
const mm = Math.floor((remaining % 3600000) / 60000);
|
||||||
const ss = Math.floor((remaining % 60000) / 1000);
|
const ss = Math.floor((remaining % 60000) / 1000);
|
||||||
if (hh > 0) {
|
if (hh > 0) {
|
||||||
return `${hh}小时${mm}分${ss}秒`;
|
return `${hh}${mm}m ${ss}s`;
|
||||||
} else if (mm > 0) {
|
} else if (mm > 0) {
|
||||||
return `${mm}分${ss}秒`;
|
return `${mm}m ${ss}s`;
|
||||||
} else {
|
} else {
|
||||||
return `${ss}秒`;
|
return `${ss}s`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return '未知时间';
|
return 'Unknown time';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理过期的请求记录
|
// Clean expired request records
|
||||||
_cleanUpRecords(ip) {
|
_cleanUpRecords(ip) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// 请求记录保留24小时
|
// Request records kept for 24 hours
|
||||||
if (this.ipRequestRecords[ip]) {
|
if (this.ipRequestRecords[ip]) {
|
||||||
this.ipRequestRecords[ip] = this.ipRequestRecords[ip].filter(
|
this.ipRequestRecords[ip] = this.ipRequestRecords[ip].filter(
|
||||||
t => now - t <= 24 * 60 * 60 * 1000
|
t => now - t <= 24 * 60 * 60 * 1000
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// 30秒触发记录同样保留24小时
|
// 30-second trigger records also kept for 24 hours
|
||||||
if (this.ip30sTriggerCount[ip]) {
|
if (this.ip30sTriggerCount[ip]) {
|
||||||
this.ip30sTriggerCount[ip] = this.ip30sTriggerCount[ip].filter(
|
this.ip30sTriggerCount[ip] = this.ip30sTriggerCount[ip].filter(
|
||||||
t => now - t <= 24 * 60 * 60 * 1000
|
t => now - t <= 24 * 60 * 60 * 1000
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// 清理过期的临时限制
|
// Clean expired temp limits
|
||||||
if (this.temporaryLimits[ip] && now >= this.temporaryLimits[ip]) {
|
if (this.temporaryLimits[ip] && now >= this.temporaryLimits[ip]) {
|
||||||
delete this.temporaryLimits[ip];
|
delete this.temporaryLimits[ip];
|
||||||
this.saveTempLimitsToFile();
|
this.saveTempLimitsToFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定期清理长期未活动的IP数据
|
// Periodically clean inactive IP data
|
||||||
cleanUpInactiveIPs() {
|
cleanUpInactiveIPs() {
|
||||||
for (const ip in this.ipRequestRecords) {
|
for (const ip in this.ipRequestRecords) {
|
||||||
const noRecentRequests = !this.ipRequestRecords[ip] || this.ipRequestRecords[ip].length === 0;
|
const noRecentRequests = !this.ipRequestRecords[ip] || this.ipRequestRecords[ip].length === 0;
|
||||||
const no30sTriggers = !this.ip30sTriggerCount[ip] || this.ip30sTriggerCount[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)) {
|
if (noRecentRequests && no30sTriggers && !this._hasTemporaryLimit(ip)) {
|
||||||
delete this.ipRequestRecords[ip];
|
delete this.ipRequestRecords[ip];
|
||||||
delete this.ip30sTriggerCount[ip];
|
delete this.ip30sTriggerCount[ip];
|
||||||
@@ -259,10 +242,10 @@ class RequestLogger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录请求并检测
|
// Record request and detect
|
||||||
async logRequest({time, ip, location, model, session}) {
|
async logRequest({time, ip, location, model, session}) {
|
||||||
if (ENABLE_DETECTION && await this.isBlacklisted(ip)) {
|
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}`;
|
const baseInfo = `IP: ${ip} | Location: ${location} | Model: ${model} | Session: ${session}`;
|
||||||
if (!ENABLE_DETECTION) {
|
if (!ENABLE_DETECTION) {
|
||||||
@@ -278,31 +261,31 @@ class RequestLogger {
|
|||||||
await ipMutex.runExclusive(() => {
|
await ipMutex.runExclusive(() => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// 检查临时限制
|
// Check temp limits
|
||||||
if (this._hasTemporaryLimit(ip)) {
|
if (this._hasTemporaryLimit(ip)) {
|
||||||
const rt = this._getRemainingTime(ip);
|
const rt = this._getRemainingTime(ip);
|
||||||
limitInfo = ` | Limit: ${this.temporaryLimits[ip] - now}ms`;
|
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]) {
|
if (!this.ipRequestRecords[ip]) {
|
||||||
this.ipRequestRecords[ip] = [];
|
this.ipRequestRecords[ip] = [];
|
||||||
}
|
}
|
||||||
this.ipRequestRecords[ip].push(now);
|
this.ipRequestRecords[ip].push(now);
|
||||||
this._cleanUpRecords(ip);
|
this._cleanUpRecords(ip);
|
||||||
|
|
||||||
// >=4次 → 加入黑名单
|
// >=4 times -> Add to blacklist
|
||||||
const requestsIn6s = this.ipRequestRecords[ip].filter(
|
const requestsIn6s = this.ipRequestRecords[ip].filter(
|
||||||
t => now - t <= 6 * 1000
|
t => now - t <= 6 * 1000
|
||||||
);
|
);
|
||||||
if (requestsIn6s.length >= 3) {
|
if (requestsIn6s.length >= 3) {
|
||||||
this.addToBlacklist(ip);
|
this.addToBlacklist(ip);
|
||||||
limitInfo = ' | Limit: PERMANENT';
|
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(
|
const requestsIn30s = this.ipRequestRecords[ip].filter(
|
||||||
t => now - t <= 30 * 1000
|
t => now - t <= 30 * 1000
|
||||||
);
|
);
|
||||||
@@ -313,19 +296,19 @@ class RequestLogger {
|
|||||||
this.ip30sTriggerCount[ip].push(now);
|
this.ip30sTriggerCount[ip].push(now);
|
||||||
this._cleanUpRecords(ip);
|
this._cleanUpRecords(ip);
|
||||||
|
|
||||||
// 24小时内触发次数 >= 5 → 限制6小时
|
// Triggered >=5 times in 24h -> limit 6 hours
|
||||||
if (this.ip30sTriggerCount[ip].length >= 5) {
|
if (this.ip30sTriggerCount[ip].length >= 5) {
|
||||||
const duration = 6 * 60 * 60 * 1000;
|
const duration = 6 * 60 * 60 * 1000;
|
||||||
this.temporaryLimits[ip] = now + duration;
|
this.temporaryLimits[ip] = now + duration;
|
||||||
limitInfo = ` | Limit: ${duration}ms`;
|
limitInfo = ` | Limit: ${duration}ms`;
|
||||||
this.saveTempLimitsToFile();
|
this.saveTempLimitsToFile();
|
||||||
throw new Error(`您在24小时内多次频繁请求,限制6小时`);
|
throw new Error(`Frequent requests in 24 hours, limited for 6 hours`);
|
||||||
} else {
|
} else {
|
||||||
const duration = 60 * 1000;
|
const duration = 60 * 1000;
|
||||||
this.temporaryLimits[ip] = now + duration;
|
this.temporaryLimits[ip] = now + duration;
|
||||||
limitInfo = ` | Limit: ${duration}ms`;
|
limitInfo = ` | Limit: ${duration}ms`;
|
||||||
this.saveTempLimitsToFile();
|
this.saveTempLimitsToFile();
|
||||||
throw new Error(`请求过多, 限制1分钟`);
|
throw new Error(`Too many requests, limited for 1 minute`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,115 +1,116 @@
|
|||||||
import os from 'os';
|
import os from 'os';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {execSync} from 'child_process';
|
import {execSync} from 'child_process';
|
||||||
|
import sysLogger from './sysLogger.mjs';
|
||||||
export function detectBrowser(preferredBrowser = 'auto') {
|
|
||||||
const platform = os.platform();
|
export function detectBrowser(preferredBrowser = 'auto') {
|
||||||
let browsers = {
|
const platform = os.platform();
|
||||||
'chrome': null,
|
let browsers = {
|
||||||
'edge': null
|
'chrome': null,
|
||||||
};
|
'edge': null
|
||||||
|
};
|
||||||
if (platform === 'win32') {
|
|
||||||
browsers.chrome = findWindowsBrowser('Chrome');
|
if (platform === 'win32') {
|
||||||
browsers.edge = findWindowsBrowser('Edge');
|
browsers.chrome = findWindowsBrowser('Chrome');
|
||||||
} else if (platform === 'darwin') {
|
browsers.edge = findWindowsBrowser('Edge');
|
||||||
browsers.chrome = findMacOSBrowser('Google Chrome');
|
} else if (platform === 'darwin') {
|
||||||
browsers.edge = findMacOSBrowser('Microsoft Edge');
|
browsers.chrome = findMacOSBrowser('Google Chrome');
|
||||||
} else if (platform === 'linux') {
|
browsers.edge = findMacOSBrowser('Microsoft Edge');
|
||||||
browsers.chrome = findLinuxBrowser('google-chrome');
|
} else if (platform === 'linux') {
|
||||||
|
browsers.chrome = findLinuxBrowser('google-chrome');
|
||||||
//Arch下AUR安装的chrome为google-chrome-stable
|
|
||||||
if (browsers.chrome == null) {
|
//Chrome installed via AUR on Arch is google-chrome-stable
|
||||||
browsers.chrome = findLinuxBrowser('google-chrome-stable');
|
if (browsers.chrome == null) {
|
||||||
}
|
browsers.chrome = findLinuxBrowser('google-chrome-stable');
|
||||||
|
}
|
||||||
browsers.edge = findLinuxBrowser('microsoft-edge');
|
|
||||||
}
|
browsers.edge = findLinuxBrowser('microsoft-edge');
|
||||||
|
}
|
||||||
if (preferredBrowser === 'auto' || preferredBrowser === undefined) {
|
|
||||||
if (browsers.chrome) {
|
if (preferredBrowser === 'auto' || preferredBrowser === undefined) {
|
||||||
return browsers.chrome;
|
if (browsers.chrome) {
|
||||||
} else if (browsers.edge) {
|
return browsers.chrome;
|
||||||
return browsers.edge;
|
} else if (browsers.edge) {
|
||||||
}
|
return browsers.edge;
|
||||||
} else if (browsers[preferredBrowser]) {
|
}
|
||||||
console.log(`使用${preferredBrowser === 'chrome' ? 'Chrome' : 'Edge'}浏览器`);
|
} else if (browsers[preferredBrowser]) {
|
||||||
return browsers[preferredBrowser];
|
sysLogger.debug(`${preferredBrowser === 'chrome' ? 'Chrome' : 'Edge'}Browser`);
|
||||||
}
|
return browsers[preferredBrowser];
|
||||||
|
}
|
||||||
console.error('未找到Chrome或Edge浏览器,请确保已安装其中之一');
|
|
||||||
process.exit(1);
|
sysLogger.error('ChromeEdgeBrowser,');
|
||||||
}
|
process.exit(1);
|
||||||
|
}
|
||||||
function findWindowsBrowser(browserName) {
|
|
||||||
const regKeys = {
|
function findWindowsBrowser(browserName) {
|
||||||
'Chrome': ['chrome.exe', 'Google\\Chrome'],
|
const regKeys = {
|
||||||
'Edge': ['msedge.exe', 'Microsoft\\Edge']
|
'Chrome': ['chrome.exe', 'Google\\Chrome'],
|
||||||
};
|
'Edge': ['msedge.exe', 'Microsoft\\Edge']
|
||||||
const [exeName, folderName] = regKeys[browserName];
|
};
|
||||||
|
const [exeName, folderName] = regKeys[browserName];
|
||||||
const regQuery = (key) => {
|
|
||||||
try {
|
const regQuery = (key) => {
|
||||||
return execSync(`reg query "${key}" /ve`).toString().trim().split('\r\n').pop().split(' ').pop();
|
try {
|
||||||
} catch (error) {
|
return execSync(`reg query "${key}" /ve`).toString().trim().split('\r\n').pop().split(' ').pop();
|
||||||
return null;
|
} catch (error) {
|
||||||
}
|
return null;
|
||||||
};
|
}
|
||||||
|
};
|
||||||
let browserPath = regQuery(`HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${exeName}`) ||
|
|
||||||
regQuery(`HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${exeName}`);
|
let browserPath = regQuery(`HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${exeName}`) ||
|
||||||
|
regQuery(`HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${exeName}`);
|
||||||
if (browserPath && fs.existsSync(browserPath)) {
|
|
||||||
return browserPath;
|
if (browserPath && fs.existsSync(browserPath)) {
|
||||||
}
|
return browserPath;
|
||||||
|
}
|
||||||
const commonPaths = [
|
|
||||||
`C:\\Program Files\\${browserName}\\Application\\${exeName}`,
|
const commonPaths = [
|
||||||
`C:\\Program Files (x86)\\${browserName}\\Application\\${exeName}`,
|
`C:\\Program Files\\${browserName}\\Application\\${exeName}`,
|
||||||
`C:\\Program Files (x86)\\Microsoft\\${browserName}\\Application\\${exeName}`,
|
`C:\\Program Files (x86)\\${browserName}\\Application\\${exeName}`,
|
||||||
`${process.env.LOCALAPPDATA}\\${browserName}\\Application\\${exeName}`,
|
`C:\\Program Files (x86)\\Microsoft\\${browserName}\\Application\\${exeName}`,
|
||||||
`${process.env.USERPROFILE}\\AppData\\Local\\${browserName}\\Application\\${exeName}`,
|
`${process.env.LOCALAPPDATA}\\${browserName}\\Application\\${exeName}`,
|
||||||
];
|
`${process.env.USERPROFILE}\\AppData\\Local\\${browserName}\\Application\\${exeName}`,
|
||||||
|
];
|
||||||
const foundPath = commonPaths.find(path => fs.existsSync(path));
|
|
||||||
if (foundPath) {
|
const foundPath = commonPaths.find(path => fs.existsSync(path));
|
||||||
return foundPath;
|
if (foundPath) {
|
||||||
}
|
return foundPath;
|
||||||
|
}
|
||||||
const userAppDataPath = process.env.LOCALAPPDATA || `${process.env.USERPROFILE}\\AppData\\Local`;
|
|
||||||
const appDataPath = path.join(userAppDataPath, folderName, 'Application');
|
const userAppDataPath = process.env.LOCALAPPDATA || `${process.env.USERPROFILE}\\AppData\\Local`;
|
||||||
|
const appDataPath = path.join(userAppDataPath, folderName, 'Application');
|
||||||
if (fs.existsSync(appDataPath)) {
|
|
||||||
const files = fs.readdirSync(appDataPath);
|
if (fs.existsSync(appDataPath)) {
|
||||||
const exePath = files.find(file => file.toLowerCase() === exeName.toLowerCase());
|
const files = fs.readdirSync(appDataPath);
|
||||||
if (exePath) {
|
const exePath = files.find(file => file.toLowerCase() === exeName.toLowerCase());
|
||||||
return path.join(appDataPath, exePath);
|
if (exePath) {
|
||||||
}
|
return path.join(appDataPath, exePath);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return null;
|
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
function findMacOSBrowser(browserName) {
|
|
||||||
const paths = [
|
function findMacOSBrowser(browserName) {
|
||||||
`/Applications/${browserName}.app/Contents/MacOS/${browserName}`,
|
const paths = [
|
||||||
`${os.homedir()}/Applications/${browserName}.app/Contents/MacOS/${browserName}`,
|
`/Applications/${browserName}.app/Contents/MacOS/${browserName}`,
|
||||||
];
|
`${os.homedir()}/Applications/${browserName}.app/Contents/MacOS/${browserName}`,
|
||||||
|
];
|
||||||
for (const path of paths) {
|
|
||||||
if (fs.existsSync(path)) {
|
for (const path of paths) {
|
||||||
return path;
|
if (fs.existsSync(path)) {
|
||||||
}
|
return path;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return null;
|
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
function findLinuxBrowser(browserName) {
|
|
||||||
try {
|
function findLinuxBrowser(browserName) {
|
||||||
return execSync(`which ${browserName}`).toString().trim();
|
try {
|
||||||
} catch (error) {
|
return execSync(`which ${browserName}`).toString().trim();
|
||||||
return null;
|
} catch (error) {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,206 +1,207 @@
|
|||||||
/**
|
import sysLogger from './sysLogger.mjs';
|
||||||
* @param {Object} page - Puppeteer
|
/**
|
||||||
* @param {Object} options - 配置选项
|
* @param {Object} page - Puppeteer
|
||||||
* @param {number} options.width - 视口宽度
|
* @param {Object} options -
|
||||||
* @param {number} options.height - 视口高度
|
* @param {number} options.width -
|
||||||
* @param {number} options.deviceScaleFactor - 设备缩放因子
|
* @param {number} options.height -
|
||||||
* @param {boolean} options.isMobile - 模拟移动设备
|
* @param {number} options.deviceScaleFactor -
|
||||||
* @param {boolean} options.hasTouch - 支持触摸
|
* @param {boolean} options.isMobile -
|
||||||
* @param {boolean} options.isLandscape - 横屏
|
* @param {boolean} options.hasTouch -
|
||||||
* @returns {Promise<void>}
|
* @param {boolean} options.isLandscape -
|
||||||
*/
|
* @returns {Promise<void>}
|
||||||
export async function fixBrowserDisplay(page, options = {}) {
|
*/
|
||||||
if (!page) {
|
export async function fixBrowserDisplay(page, options = {}) {
|
||||||
console.error('页面对象为空,无法修复显示');
|
if (!page) {
|
||||||
return;
|
sysLogger.error('Page object is empty, unable to fix display');
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
const defaultOptions = {
|
|
||||||
width: 1280,
|
const defaultOptions = {
|
||||||
height: 800,
|
width: 1280,
|
||||||
deviceScaleFactor: 1,
|
height: 800,
|
||||||
isMobile: false,
|
deviceScaleFactor: 1,
|
||||||
hasTouch: false,
|
isMobile: false,
|
||||||
isLandscape: true
|
hasTouch: false,
|
||||||
};
|
isLandscape: true
|
||||||
|
};
|
||||||
const settings = {...defaultOptions, ...options};
|
|
||||||
|
const settings = {...defaultOptions, ...options};
|
||||||
try {
|
|
||||||
// 设置视口大小和设备比例
|
try {
|
||||||
await page.setViewport({
|
// Set viewport size and device ratio
|
||||||
width: settings.width,
|
await page.setViewport({
|
||||||
height: settings.height,
|
width: settings.width,
|
||||||
deviceScaleFactor: settings.deviceScaleFactor,
|
height: settings.height,
|
||||||
isMobile: settings.isMobile,
|
deviceScaleFactor: settings.deviceScaleFactor,
|
||||||
hasTouch: settings.hasTouch,
|
isMobile: settings.isMobile,
|
||||||
isLandscape: settings.isLandscape
|
hasTouch: settings.hasTouch,
|
||||||
});
|
isLandscape: settings.isLandscape
|
||||||
|
});
|
||||||
// 尝试调整窗口大小
|
|
||||||
const session = await page.target().createCDPSession();
|
// Try resizing window
|
||||||
await session.send('Emulation.setDeviceMetricsOverride', {
|
const session = await page.target().createCDPSession();
|
||||||
width: settings.width,
|
await session.send('Emulation.setDeviceMetricsOverride', {
|
||||||
height: settings.height,
|
width: settings.width,
|
||||||
deviceScaleFactor: settings.deviceScaleFactor,
|
height: settings.height,
|
||||||
mobile: settings.isMobile,
|
deviceScaleFactor: settings.deviceScaleFactor,
|
||||||
screenWidth: settings.width,
|
mobile: settings.isMobile,
|
||||||
screenHeight: settings.height
|
screenWidth: settings.width,
|
||||||
});
|
screenHeight: settings.height
|
||||||
|
});
|
||||||
// 重置页面缩放
|
|
||||||
await page.evaluate(() => {
|
// Reset page zoom
|
||||||
document.body.style.zoom = '100%';
|
await page.evaluate(() => {
|
||||||
document.body.style.transform = 'scale(1)';
|
document.body.style.zoom = '100%';
|
||||||
document.body.style.transformOrigin = '0 0';
|
document.body.style.transform = 'scale(1)';
|
||||||
|
document.body.style.transformOrigin = '0 0';
|
||||||
// 尝试修复可能存在的CSS
|
|
||||||
const styleElement = document.createElement('style');
|
// Attempt to fix possible CSS
|
||||||
styleElement.textContent = `
|
const styleElement = document.createElement('style');
|
||||||
html, body {
|
styleElement.textContent = `
|
||||||
width: 100% !important;
|
html, body {
|
||||||
height: 100% !important;
|
width: 100% !important;
|
||||||
overflow: auto !important;
|
height: 100% !important;
|
||||||
}
|
overflow: auto !important;
|
||||||
|
}
|
||||||
.container, .main, #app, #root {
|
|
||||||
max-width: 100% !important;
|
.container, .main, #app, #root {
|
||||||
width: auto !important;
|
max-width: 100% !important;
|
||||||
}
|
width: auto !important;
|
||||||
`;
|
}
|
||||||
document.head.appendChild(styleElement);
|
`;
|
||||||
|
document.head.appendChild(styleElement);
|
||||||
window.dispatchEvent(new Event('resize'));
|
|
||||||
});
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
});
|
||||||
} catch (error) {
|
|
||||||
console.error('修复浏览器显示时出错:', error);
|
} catch (error) {
|
||||||
}
|
sysLogger.error('Browser:', error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 调整CSS比例
|
/**
|
||||||
* @param {Object} page - Puppeteer
|
* CSS
|
||||||
* @param {number} scale - 缩放比例
|
* @param {Object} page - Puppeteer
|
||||||
* @returns {Promise<void>}
|
* @param {number} scale -
|
||||||
*/
|
* @returns {Promise<void>}
|
||||||
export async function adjustCssScaling(page, scale = 1) {
|
*/
|
||||||
if (!page) return;
|
export async function adjustCssScaling(page, scale = 1) {
|
||||||
|
if (!page) return;
|
||||||
try {
|
|
||||||
await page.evaluate((scale) => {
|
try {
|
||||||
const styleElem = document.createElement('style');
|
await page.evaluate((scale) => {
|
||||||
styleElem.id = 'puppeteer-display-fix';
|
const styleElem = document.createElement('style');
|
||||||
styleElem.textContent = `
|
styleElem.id = 'puppeteer-display-fix';
|
||||||
html {
|
styleElem.textContent = `
|
||||||
transform: scale(${scale});
|
html {
|
||||||
transform-origin: top left;
|
transform: scale(${scale});
|
||||||
width: ${100 / scale}% !important;
|
transform-origin: top left;
|
||||||
height: ${100 / scale}% !important;
|
width: ${100 / scale}% !important;
|
||||||
}
|
height: ${100 / scale}% !important;
|
||||||
`;
|
}
|
||||||
document.head.appendChild(styleElem);
|
`;
|
||||||
|
document.head.appendChild(styleElem);
|
||||||
// 重新计算布局
|
|
||||||
window.dispatchEvent(new Event('resize'));
|
// Recalculate layout
|
||||||
}, scale);
|
window.dispatchEvent(new Event('resize'));
|
||||||
} catch (error) {
|
}, scale);
|
||||||
console.error('调整CSS比例时出错:', error);
|
} catch (error) {
|
||||||
}
|
sysLogger.error('Error adjusting CSS ratio:', error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 修复高DPI
|
/**
|
||||||
* @param {Object} page - Puppeteer
|
* DPI
|
||||||
* @returns {Promise<void>}
|
* @param {Object} page - Puppeteer
|
||||||
*/
|
* @returns {Promise<void>}
|
||||||
export async function fixHighDpiDisplay(page) {
|
*/
|
||||||
if (!page) return;
|
export async function fixHighDpiDisplay(page) {
|
||||||
|
if (!page) return;
|
||||||
try {
|
|
||||||
// 检测设备像素比
|
try {
|
||||||
const devicePixelRatio = await page.evaluate(() => window.devicePixelRatio);
|
// Detect device pixel ratio
|
||||||
|
const devicePixelRatio = await page.evaluate(() => window.devicePixelRatio);
|
||||||
if (devicePixelRatio > 1) {
|
|
||||||
await page.setViewport({
|
if (devicePixelRatio > 1) {
|
||||||
width: 1280,
|
await page.setViewport({
|
||||||
height: 800,
|
width: 1280,
|
||||||
deviceScaleFactor: devicePixelRatio
|
height: 800,
|
||||||
});
|
deviceScaleFactor: devicePixelRatio
|
||||||
|
});
|
||||||
await page.evaluate((dpr) => {
|
|
||||||
const meta = document.createElement('meta');
|
await page.evaluate((dpr) => {
|
||||||
meta.setAttribute('name', 'viewport');
|
const meta = document.createElement('meta');
|
||||||
meta.setAttribute('content', `initial-scale=1, minimum-scale=1, maximum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi, user-scalable=no`);
|
meta.setAttribute('name', 'viewport');
|
||||||
document.head.appendChild(meta);
|
meta.setAttribute('content', `initial-scale=1, minimum-scale=1, maximum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi, user-scalable=no`);
|
||||||
}, devicePixelRatio);
|
document.head.appendChild(meta);
|
||||||
}
|
}, devicePixelRatio);
|
||||||
} catch (error) {
|
}
|
||||||
console.error('修复高DPI显示时出错:', error);
|
} catch (error) {
|
||||||
}
|
sysLogger.error('Fix High-DPI display:', error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 完整浏览器显示优化
|
/**
|
||||||
* @param {Object} page - Puppeteer
|
* Browser
|
||||||
* @param {Object} options - 配置
|
* @param {Object} page - Puppeteer
|
||||||
* @returns {Promise<void>}
|
* @param {Object} options -
|
||||||
*/
|
* @returns {Promise<void>}
|
||||||
export async function optimizeBrowserDisplay(page, options = {}) {
|
*/
|
||||||
const defaultOptions = {
|
export async function optimizeBrowserDisplay(page, options = {}) {
|
||||||
width: 1280,
|
const defaultOptions = {
|
||||||
height: 800,
|
width: 1280,
|
||||||
deviceScaleFactor: 1,
|
height: 800,
|
||||||
cssScale: null,
|
deviceScaleFactor: 1,
|
||||||
fixHighDpi: true,
|
cssScale: null,
|
||||||
forceResize: true
|
fixHighDpi: true,
|
||||||
};
|
forceResize: true
|
||||||
|
};
|
||||||
const config = {...defaultOptions, ...options};
|
|
||||||
|
const config = {...defaultOptions, ...options};
|
||||||
try {
|
|
||||||
// 基本显示修复
|
try {
|
||||||
await fixBrowserDisplay(page, {
|
// Basic display fix
|
||||||
width: config.width,
|
await fixBrowserDisplay(page, {
|
||||||
height: config.height,
|
width: config.width,
|
||||||
deviceScaleFactor: config.deviceScaleFactor
|
height: config.height,
|
||||||
});
|
deviceScaleFactor: config.deviceScaleFactor
|
||||||
|
});
|
||||||
// 修复高DPI显示
|
|
||||||
if (config.fixHighDpi) {
|
// Fix High-DPI display
|
||||||
await fixHighDpiDisplay(page);
|
if (config.fixHighDpi) {
|
||||||
}
|
await fixHighDpiDisplay(page);
|
||||||
|
}
|
||||||
if (config.cssScale !== null) {
|
|
||||||
await adjustCssScaling(page, config.cssScale);
|
if (config.cssScale !== null) {
|
||||||
}
|
await adjustCssScaling(page, config.cssScale);
|
||||||
|
}
|
||||||
// 如果强制调整窗口大小
|
|
||||||
if (config.forceResize && !config.isHeadless) {
|
// If forced to resize window
|
||||||
try {
|
if (config.forceResize && !config.isHeadless) {
|
||||||
const client = await page.target().createCDPSession();
|
try {
|
||||||
await client.send('Browser.getWindowForTarget');
|
const client = await page.target().createCDPSession();
|
||||||
await client.send('Browser.setWindowBounds', {
|
await client.send('Browser.getWindowForTarget');
|
||||||
windowId: 1,
|
await client.send('Browser.setWindowBounds', {
|
||||||
bounds: {
|
windowId: 1,
|
||||||
width: config.width,
|
bounds: {
|
||||||
height: config.height
|
width: config.width,
|
||||||
}
|
height: config.height
|
||||||
});
|
}
|
||||||
} catch (resizeError) {
|
});
|
||||||
// console.log('无法调整窗口大小:', resizeError.message);
|
} catch (resizeError) {
|
||||||
|
// sysLogger.debug('Unable to resize window:', resizeError.message);
|
||||||
try {
|
|
||||||
await page.evaluate((width, height) => {
|
try {
|
||||||
window.resizeTo(width, height);
|
await page.evaluate((width, height) => {
|
||||||
}, config.width, config.height);
|
window.resizeTo(width, height);
|
||||||
} catch (altError) {
|
}, config.width, config.height);
|
||||||
console.log('失败:', altError.message);
|
} catch (altError) {
|
||||||
}
|
sysLogger.debug('Failed:', altError.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('浏览器显示优化失败:', error);
|
} catch (error) {
|
||||||
}
|
sysLogger.error('BrowserDisplay optimization failed:', error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,242 +1,210 @@
|
|||||||
import * as docx from "docx";
|
import * as docx from "docx";
|
||||||
import cookie from "cookie";
|
import cookie from "cookie";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import {execSync} from "child_process";
|
import {execSync} from "child_process";
|
||||||
|
import sysLogger from './sysLogger.mjs';
|
||||||
function getGitRevision() {
|
|
||||||
// get git revision and branch
|
let cachedGitRevision = null;
|
||||||
try {
|
|
||||||
const revision = execSync("git rev-parse --short HEAD", {stdio: "pipe"}).toString().trim();
|
function getGitRevision() {
|
||||||
const branch = execSync("git rev-parse --abbrev-ref HEAD", {stdio: "pipe"}).toString().trim();
|
if (cachedGitRevision) {
|
||||||
return {revision, branch};
|
return cachedGitRevision;
|
||||||
} catch (e) {
|
}
|
||||||
return {revision: "unknown", branch: "unknown"};
|
// 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();
|
||||||
// 创建目录
|
cachedGitRevision = {revision, branch};
|
||||||
function createDirectoryIfNotExists(dirPath) {
|
return cachedGitRevision;
|
||||||
if (!fs.existsSync(dirPath)) {
|
} catch (e) {
|
||||||
fs.mkdirSync(dirPath, {recursive: true});
|
cachedGitRevision = {revision: "unknown", branch: "unknown"};
|
||||||
}
|
return cachedGitRevision;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function extractCookie(cookies) {
|
|
||||||
let jwtSession = null;
|
// Create directory
|
||||||
let jwtToken = null;
|
function createDirectoryIfNotExists(dirPath) {
|
||||||
let ds = null;
|
if (!fs.existsSync(dirPath)) {
|
||||||
let dsr = null;
|
fs.mkdirSync(dirPath, {recursive: true});
|
||||||
let you_subscription = null;
|
}
|
||||||
let youpro_subscription = null;
|
}
|
||||||
|
|
||||||
cookies = cookie.parse(cookies);
|
function extractCookie(cookies) {
|
||||||
if (cookies["stytch_session"]) jwtSession = cookies["stytch_session"];
|
let jwtSession = null;
|
||||||
if (cookies["stytch_session_jwt"]) jwtToken = cookies["stytch_session_jwt"];
|
let jwtToken = null;
|
||||||
if (cookies["DS"]) ds = cookies["DS"];
|
let ds = null;
|
||||||
if (cookies["DSR"]) dsr = cookies["DSR"];
|
let dsr = null;
|
||||||
if (cookies["you_subscription"]) you_subscription = cookies["you_subscription"];
|
let you_subscription = null;
|
||||||
if (cookies["youpro_subscription"]) youpro_subscription = cookies["youpro_subscription"];
|
let youpro_subscription = null;
|
||||||
|
|
||||||
return {jwtSession, jwtToken, ds, dsr, you_subscription, youpro_subscription};
|
cookies = cookie.parse(cookies);
|
||||||
}
|
if (cookies["stytch_session"]) jwtSession = cookies["stytch_session"];
|
||||||
|
if (cookies["stytch_session_jwt"]) jwtToken = cookies["stytch_session_jwt"];
|
||||||
function getSessionCookie(jwtSession, jwtToken, ds, dsr, you_subscription, youpro_subscription) {
|
if (cookies["DS"]) ds = cookies["DS"];
|
||||||
let sessionCookie = [];
|
if (cookies["DSR"]) dsr = cookies["DSR"];
|
||||||
|
if (cookies["you_subscription"]) you_subscription = cookies["you_subscription"];
|
||||||
// 处理旧版 cookie
|
if (cookies["youpro_subscription"]) youpro_subscription = cookies["youpro_subscription"];
|
||||||
if (jwtSession && jwtToken) {
|
|
||||||
sessionCookie = [
|
return {jwtSession, jwtToken, ds, dsr, you_subscription, youpro_subscription};
|
||||||
{
|
}
|
||||||
name: "stytch_session",
|
|
||||||
value: jwtSession,
|
function getSessionCookie(jwtSession, jwtToken, ds, dsr, you_subscription, youpro_subscription) {
|
||||||
domain: "you.com",
|
let sessionCookie = [];
|
||||||
path: "/",
|
|
||||||
expires: 1800000000,
|
// Process legacy cookie
|
||||||
httpOnly: false,
|
if (jwtSession && jwtToken) {
|
||||||
secure: true,
|
sessionCookie = [
|
||||||
sameSite: "Lax",
|
{
|
||||||
},
|
name: "stytch_session",
|
||||||
{
|
value: jwtSession,
|
||||||
name: "ydc_stytch_session",
|
domain: "you.com",
|
||||||
value: jwtSession,
|
path: "/",
|
||||||
domain: "you.com",
|
expires: 1800000000,
|
||||||
path: "/",
|
httpOnly: false,
|
||||||
expires: 1800000000,
|
secure: true,
|
||||||
httpOnly: true,
|
sameSite: "Lax",
|
||||||
secure: true,
|
},
|
||||||
sameSite: "Lax",
|
{
|
||||||
},
|
name: "ydc_stytch_session",
|
||||||
{
|
value: jwtSession,
|
||||||
name: "stytch_session_jwt",
|
domain: "you.com",
|
||||||
value: jwtToken,
|
path: "/",
|
||||||
domain: "you.com",
|
expires: 1800000000,
|
||||||
path: "/",
|
httpOnly: true,
|
||||||
expires: 1800000000,
|
secure: true,
|
||||||
httpOnly: false,
|
sameSite: "Lax",
|
||||||
secure: true,
|
},
|
||||||
sameSite: "Lax",
|
{
|
||||||
},
|
name: "stytch_session_jwt",
|
||||||
{
|
value: jwtToken,
|
||||||
name: "ydc_stytch_session_jwt",
|
domain: "you.com",
|
||||||
value: jwtToken,
|
path: "/",
|
||||||
domain: "you.com",
|
expires: 1800000000,
|
||||||
path: "/",
|
httpOnly: false,
|
||||||
expires: 1800000000,
|
secure: true,
|
||||||
httpOnly: true,
|
sameSite: "Lax",
|
||||||
secure: true,
|
},
|
||||||
sameSite: "Lax",
|
{
|
||||||
}
|
name: "ydc_stytch_session_jwt",
|
||||||
];
|
value: jwtToken,
|
||||||
}
|
domain: "you.com",
|
||||||
|
path: "/",
|
||||||
// 处理新版 cookie
|
expires: 1800000000,
|
||||||
if (ds) {
|
httpOnly: true,
|
||||||
sessionCookie.push({
|
secure: true,
|
||||||
name: "DS",
|
sameSite: "Lax",
|
||||||
value: ds,
|
}
|
||||||
domain: "you.com",
|
];
|
||||||
path: "/",
|
}
|
||||||
expires: 1800000000,
|
|
||||||
httpOnly: false,
|
// Process new cookie
|
||||||
secure: true,
|
if (ds) {
|
||||||
sameSite: "Lax",
|
sessionCookie.push({
|
||||||
});
|
name: "DS",
|
||||||
}
|
value: ds,
|
||||||
if (dsr) {
|
domain: "you.com",
|
||||||
sessionCookie.push({
|
path: "/",
|
||||||
name: "DSR",
|
expires: 1800000000,
|
||||||
value: dsr,
|
httpOnly: false,
|
||||||
domain: "you.com",
|
secure: true,
|
||||||
path: "/",
|
sameSite: "Lax",
|
||||||
expires: 1800000000,
|
});
|
||||||
httpOnly: false,
|
}
|
||||||
secure: true,
|
if (dsr) {
|
||||||
sameSite: "Lax",
|
sessionCookie.push({
|
||||||
});
|
name: "DSR",
|
||||||
}
|
value: dsr,
|
||||||
|
domain: "you.com",
|
||||||
if (you_subscription) {
|
path: "/",
|
||||||
sessionCookie.push({
|
expires: 1800000000,
|
||||||
name: "you_subscription",
|
httpOnly: false,
|
||||||
value: you_subscription,
|
secure: true,
|
||||||
domain: "you.com",
|
sameSite: "Lax",
|
||||||
path: "/",
|
});
|
||||||
expires: 1800000000,
|
}
|
||||||
httpOnly: false,
|
|
||||||
secure: true,
|
if (you_subscription) {
|
||||||
sameSite: "Lax",
|
sessionCookie.push({
|
||||||
});
|
name: "you_subscription",
|
||||||
}
|
value: you_subscription,
|
||||||
|
domain: "you.com",
|
||||||
if (youpro_subscription) {
|
path: "/",
|
||||||
sessionCookie.push({
|
expires: 1800000000,
|
||||||
name: "youpro_subscription",
|
httpOnly: false,
|
||||||
value: youpro_subscription,
|
secure: true,
|
||||||
domain: "you.com",
|
sameSite: "Lax",
|
||||||
path: "/",
|
});
|
||||||
expires: 1800000000,
|
}
|
||||||
httpOnly: false,
|
|
||||||
secure: true,
|
if (youpro_subscription) {
|
||||||
sameSite: "Lax",
|
sessionCookie.push({
|
||||||
});
|
name: "youpro_subscription",
|
||||||
}
|
value: youpro_subscription,
|
||||||
|
domain: "you.com",
|
||||||
// 添加隐身模式 cookie(如果启用)
|
path: "/",
|
||||||
if (process.env.INCOGNITO_MODE === "true") {
|
expires: 1800000000,
|
||||||
sessionCookie.push({
|
httpOnly: false,
|
||||||
name: "incognito",
|
secure: true,
|
||||||
value: "true",
|
sameSite: "Lax",
|
||||||
domain: "you.com",
|
});
|
||||||
path: "/",
|
}
|
||||||
expires: 1800000000,
|
|
||||||
secure: true,
|
// Add incognito mode cookie (if enabled)
|
||||||
});
|
if (process.env.INCOGNITO_MODE === "true") {
|
||||||
}
|
sessionCookie.push({
|
||||||
return sessionCookie;
|
name: "incognito",
|
||||||
}
|
value: "true",
|
||||||
|
domain: "you.com",
|
||||||
function sleep(ms) {
|
path: "/",
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
expires: 1800000000,
|
||||||
}
|
secure: true,
|
||||||
|
});
|
||||||
function createDocx(content) {
|
}
|
||||||
let paragraphs = [];
|
return sessionCookie;
|
||||||
content.split("\n").forEach((line) => {
|
}
|
||||||
paragraphs.push(
|
|
||||||
new docx.Paragraph({
|
function sleep(ms) {
|
||||||
children: [new docx.TextRun(line)],
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
})
|
}
|
||||||
);
|
|
||||||
});
|
function createDocx(content) {
|
||||||
let doc = new docx.Document({
|
let paragraphs = [];
|
||||||
sections: [
|
content.split("\n").forEach((line) => {
|
||||||
{
|
paragraphs.push(
|
||||||
properties: {},
|
new docx.Paragraph({
|
||||||
children: paragraphs,
|
children: [new docx.TextRun(line)],
|
||||||
},
|
})
|
||||||
],
|
);
|
||||||
});
|
});
|
||||||
return docx.Packer.toBuffer(doc).then((buffer) => buffer);
|
let doc = new docx.Document({
|
||||||
}
|
sections: [
|
||||||
|
{
|
||||||
// eventStream util
|
properties: {},
|
||||||
function createEvent(event, data) {
|
children: paragraphs,
|
||||||
// if data is object, stringify it
|
},
|
||||||
if (typeof data === "object") {
|
],
|
||||||
data = JSON.stringify(data);
|
});
|
||||||
}
|
return docx.Packer.toBuffer(doc).then((buffer) => buffer);
|
||||||
return `event: ${event}\ndata: ${data}\n\n`;
|
}
|
||||||
}
|
|
||||||
|
// eventStream util
|
||||||
function extractPerplexityCookie(cookieString) {
|
function createEvent(event, data) {
|
||||||
const cookies = cookie.parse(cookieString);
|
// if data is object, stringify it
|
||||||
return {
|
if (typeof data === "object") {
|
||||||
sessionToken: cookies['__Secure-next-auth.session-token'],
|
data = JSON.stringify(data);
|
||||||
isIncognito: cookies['pplx.is-incognito'] === 'true'
|
}
|
||||||
};
|
if (event === "data") {
|
||||||
}
|
return `data: ${data}\n\n`;
|
||||||
|
}
|
||||||
function getPerplexitySessionCookie(extractedCookie) {
|
return `event: ${event}\ndata: ${data}\n\n`;
|
||||||
let sessionCookie = [];
|
}
|
||||||
|
|
||||||
if (extractedCookie.sessionToken) {
|
export {
|
||||||
sessionCookie.push({
|
createEvent,
|
||||||
name: "__Secure-next-auth.session-token",
|
createDirectoryIfNotExists,
|
||||||
value: extractedCookie.sessionToken,
|
sleep,
|
||||||
domain: "www.perplexity.ai",
|
extractCookie,
|
||||||
path: "/",
|
getSessionCookie,
|
||||||
expires: 1800000000,
|
createDocx,
|
||||||
httpOnly: true,
|
getGitRevision,
|
||||||
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,
|
|
||||||
sleep,
|
|
||||||
extractCookie,
|
|
||||||
getSessionCookie,
|
|
||||||
createDocx,
|
|
||||||
getGitRevision,
|
|
||||||
extractPerplexityCookie,
|
|
||||||
getPerplexitySessionCookie
|
|
||||||
};
|
};
|
||||||
@@ -1,153 +1,154 @@
|
|||||||
import {exec, execSync} from 'child_process';
|
import {exec, execSync} from 'child_process';
|
||||||
import {promisify} from 'util';
|
import {promisify} from 'util';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import {fileURLToPath} from 'url';
|
import {fileURLToPath} from 'url';
|
||||||
import {setupBrowserFingerprint} from './browserFingerprint.mjs';
|
import {setupBrowserFingerprint} from './browserFingerprint.mjs';
|
||||||
|
import sysLogger from './sysLogger.mjs';
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const execPromise = promisify(exec);
|
const __dirname = path.dirname(__filename);
|
||||||
|
const execPromise = promisify(exec);
|
||||||
/**
|
|
||||||
* @param {string} userDataDir - 用户数据目录
|
/**
|
||||||
* @param {string} edgePath - Edge浏览器路径
|
* @param {string} userDataDir -
|
||||||
* @param {number} debugPort - 调试端口
|
* @param {string} edgePath - EdgeBrowser
|
||||||
* @returns {Promise<object>} - browser和page
|
* @param {number} debugPort -
|
||||||
*/
|
* @returns {Promise<object>} - browserpage
|
||||||
export async function launchEdgeBrowser(userDataDir, edgePath, debugPort = 9222) {
|
*/
|
||||||
if (!fs.existsSync(userDataDir)) {
|
export async function launchEdgeBrowser(userDataDir, edgePath, debugPort = 9222) {
|
||||||
fs.mkdirSync(userDataDir, {recursive: true});
|
if (!fs.existsSync(userDataDir)) {
|
||||||
}
|
fs.mkdirSync(userDataDir, {recursive: true});
|
||||||
|
}
|
||||||
// 关闭可能的Edge进程
|
|
||||||
try {
|
// Close possible Edge processes
|
||||||
if (os.platform() === 'win32') {
|
try {
|
||||||
await execPromise('taskkill /f /im msedge.exe').catch(() => {
|
if (os.platform() === 'win32') {
|
||||||
});
|
await execPromise('taskkill /f /im msedge.exe').catch(() => {
|
||||||
} else if (os.platform() === 'darwin') {
|
});
|
||||||
// macOS
|
} else if (os.platform() === 'darwin') {
|
||||||
await execPromise('pkill -f "Microsoft Edge"').catch(() => {
|
// macOS
|
||||||
});
|
await execPromise('pkill -f "Microsoft Edge"').catch(() => {
|
||||||
} else {
|
});
|
||||||
// Linux
|
} else {
|
||||||
await execPromise('pkill -f "microsoft-edge"').catch(() => {
|
// Linux
|
||||||
});
|
await execPromise('pkill -f "microsoft-edge"').catch(() => {
|
||||||
}
|
});
|
||||||
} catch (e) {
|
}
|
||||||
}
|
} catch (e) {
|
||||||
|
}
|
||||||
const remoteDebuggingPort = debugPort;
|
|
||||||
let edgeProcess;
|
const remoteDebuggingPort = debugPort;
|
||||||
|
let edgeProcess;
|
||||||
const args = [
|
|
||||||
`--remote-debugging-port=${remoteDebuggingPort}`,
|
const args = [
|
||||||
`--user-data-dir="${userDataDir}"`,
|
`--remote-debugging-port=${remoteDebuggingPort}`,
|
||||||
'--no-first-run',
|
`--user-data-dir="${userDataDir}"`,
|
||||||
'--no-default-browser-check',
|
'--no-first-run',
|
||||||
'--disable-popup-blocking',
|
'--no-default-browser-check',
|
||||||
'--disable-infobars',
|
'--disable-popup-blocking',
|
||||||
'--disable-translate',
|
'--disable-infobars',
|
||||||
'--disable-sync',
|
'--disable-translate',
|
||||||
'--window-size=1280,850',
|
'--disable-sync',
|
||||||
'--force-device-scale-factor=1',
|
'--window-size=1280,850',
|
||||||
'about:blank' // 打开空白页
|
'--force-device-scale-factor=1',
|
||||||
];
|
'about:blank' // Open blank page
|
||||||
|
];
|
||||||
try {
|
|
||||||
// 启动Edge浏览器
|
try {
|
||||||
const cmdArgs = args.join(' ');
|
// EdgeBrowser
|
||||||
const cmd = `"${edgePath}" ${cmdArgs}`;
|
const cmdArgs = args.join(' ');
|
||||||
edgeProcess = exec(cmd);
|
const cmd = `"${edgePath}" ${cmdArgs}`;
|
||||||
|
edgeProcess = exec(cmd);
|
||||||
console.log(`等待Edge浏览器启动...`);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
sysLogger.debug(`Waiting for Edge browser to start...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
const puppeteer = await import('puppeteer-core');
|
|
||||||
|
const puppeteer = await import('puppeteer-core');
|
||||||
// 连接到浏览器
|
|
||||||
const browser = await puppeteer.connect({
|
// Browser
|
||||||
browserURL: `http://127.0.0.1:${remoteDebuggingPort}`,
|
const browser = await puppeteer.connect({
|
||||||
defaultViewport: {width: 1280, height: 850}
|
browserURL: `http://127.0.0.1:${remoteDebuggingPort}`,
|
||||||
});
|
defaultViewport: {width: 1280, height: 850}
|
||||||
|
});
|
||||||
// 获取第一个页面
|
|
||||||
const pages = await browser.pages();
|
// Get first page
|
||||||
let page = pages[0];
|
const pages = await browser.pages();
|
||||||
if (!page) {
|
let page = pages[0];
|
||||||
page = await browser.newPage();
|
if (!page) {
|
||||||
}
|
page = await browser.newPage();
|
||||||
|
}
|
||||||
const originalUserAgent = await page.evaluate(() => navigator.userAgent);
|
|
||||||
// console.log(`Edge浏览器原始用户代理: ${originalUserAgent}`);
|
const originalUserAgent = await page.evaluate(() => navigator.userAgent);
|
||||||
|
// sysLogger.debug(`EdgeBrowser: ${originalUserAgent}`);
|
||||||
// 应用随机指纹
|
|
||||||
const fingerprint = await setupBrowserFingerprint(page, 'edge');
|
// Apply random fingerprint
|
||||||
|
const fingerprint = await setupBrowserFingerprint(page, 'edge');
|
||||||
// 验证指纹是否成功应用
|
|
||||||
const newUserAgent = await page.evaluate(() => navigator.userAgent);
|
// Verify if fingerprint was successfully applied
|
||||||
// console.log(`Edge浏览器应用指纹后用户代理: ${newUserAgent}`);
|
const newUserAgent = await page.evaluate(() => navigator.userAgent);
|
||||||
|
// sysLogger.debug(`EdgeBrowserApply fingerprint: ${newUserAgent}`);
|
||||||
if (!newUserAgent.includes('Edg')) {
|
|
||||||
console.warn(`警告: 浏览器可能不是Edge。`);
|
if (!newUserAgent.includes('Edg')) {
|
||||||
}
|
sysLogger.warn(`: BrowserEdge。`);
|
||||||
|
}
|
||||||
return {
|
|
||||||
browser,
|
return {
|
||||||
page,
|
browser,
|
||||||
process: edgeProcess,
|
page,
|
||||||
fingerprint: fingerprint
|
process: edgeProcess,
|
||||||
};
|
fingerprint: fingerprint
|
||||||
} catch (error) {
|
};
|
||||||
console.error(`启动Edge浏览器失败:`, error);
|
} catch (error) {
|
||||||
|
sysLogger.error(`EdgeBrowserFailed:`, error);
|
||||||
if (edgeProcess) {
|
|
||||||
try {
|
if (edgeProcess) {
|
||||||
edgeProcess.kill();
|
try {
|
||||||
} catch (e) {
|
edgeProcess.kill();
|
||||||
}
|
} catch (e) {
|
||||||
}
|
}
|
||||||
|
}
|
||||||
throw error;
|
|
||||||
}
|
throw error;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 查找Edge浏览器路径
|
/**
|
||||||
* @returns {string|null}
|
* EdgeBrowser
|
||||||
*/
|
* @returns {string|null}
|
||||||
export function findEdgePath() {
|
*/
|
||||||
const platform = os.platform();
|
export function findEdgePath() {
|
||||||
|
const platform = os.platform();
|
||||||
if (platform === 'win32') {
|
|
||||||
const commonPaths = [
|
if (platform === 'win32') {
|
||||||
`${process.env['ProgramFiles(x86)']}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
const commonPaths = [
|
||||||
`${process.env.ProgramFiles}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
`${process.env['ProgramFiles(x86)']}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
||||||
`${process.env.LOCALAPPDATA}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
`${process.env.ProgramFiles}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
||||||
`C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe`,
|
`${process.env.LOCALAPPDATA}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
||||||
`C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe`
|
`C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe`,
|
||||||
];
|
`C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe`
|
||||||
|
];
|
||||||
for (const path of commonPaths) {
|
|
||||||
if (fs.existsSync(path)) {
|
for (const path of commonPaths) {
|
||||||
return path;
|
if (fs.existsSync(path)) {
|
||||||
}
|
return path;
|
||||||
}
|
}
|
||||||
} else if (platform === 'darwin') {
|
}
|
||||||
const macPath = '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge';
|
} else if (platform === 'darwin') {
|
||||||
if (fs.existsSync(macPath)) {
|
const macPath = '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge';
|
||||||
return macPath;
|
if (fs.existsSync(macPath)) {
|
||||||
}
|
return macPath;
|
||||||
} else {
|
}
|
||||||
// Linux
|
} else {
|
||||||
try {
|
// Linux
|
||||||
const {stdout} = execSync('which microsoft-edge');
|
try {
|
||||||
if (stdout && stdout.trim()) {
|
const {stdout} = execSync('which microsoft-edge');
|
||||||
return stdout.trim();
|
if (stdout && stdout.trim()) {
|
||||||
}
|
return stdout.trim();
|
||||||
} catch (e) {
|
}
|
||||||
}
|
} catch (e) {
|
||||||
}
|
}
|
||||||
return null;
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
@@ -1,204 +1,205 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import {fileURLToPath} from "url";
|
import {fileURLToPath} from "url";
|
||||||
import {Mutex} from "async-mutex";
|
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 __filename = fileURLToPath(import.meta.url);
|
||||||
const CONFIG_FILE_PATH = path.join(__dirname, "../config.mjs");
|
const __dirname = path.dirname(__filename);
|
||||||
|
const CONFIG_FILE_PATH = path.join(process.cwd(), "config.mjs");
|
||||||
// 仅在 USE_MANUAL_LOGIN 为 false 且 ENABLE_AUTO_COOKIE_UPDATE 为 true 时生效
|
|
||||||
const ENABLE_AUTO_COOKIE_UPDATE = process.env.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) {
|
|
||||||
// 正则匹配 `` `...` ``
|
function unifyQuotesForJSON(str) {
|
||||||
let out = str.replace(/`([^`]*)`/g, (match, p1) => {
|
// Regex match `` `...` ``
|
||||||
const safe = p1.replace(/"/g, '\\"');
|
let out = str.replace(/`([^`]*)`/g, (match, p1) => {
|
||||||
return `"${safe}"`;
|
const safe = p1.replace(/"/g, '\\"');
|
||||||
});
|
return `"${safe}"`;
|
||||||
out = out.replace(/'([^']*)'/g, (match, p1) => {
|
});
|
||||||
const safe = p1.replace(/"/g, '\\"');
|
out = out.replace(/'([^']*)'/g, (match, p1) => {
|
||||||
return `"${safe}"`;
|
const safe = p1.replace(/"/g, '\\"');
|
||||||
});
|
return `"${safe}"`;
|
||||||
|
});
|
||||||
return out;
|
|
||||||
}
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* cookies 解析出 DS 与 DSR
|
/**
|
||||||
* @param {Array} cookies 获取到的 cookie 数组
|
* cookies DS DSR
|
||||||
* @returns {{ ds?: string, dsr?: string }}
|
* @param {Array} cookies cookie
|
||||||
*/
|
* @returns {{ ds?: string, dsr?: string }}
|
||||||
function parseDSAndDSR(cookies) {
|
*/
|
||||||
let dsValue, dsrValue;
|
function parseDSAndDSR(cookies) {
|
||||||
for (const c of cookies) {
|
let dsValue, dsrValue;
|
||||||
if (c.name === "DS") {
|
for (const c of cookies) {
|
||||||
dsValue = c.value;
|
if (c.name === "DS") {
|
||||||
} else if (c.name === "DSR") {
|
dsValue = c.value;
|
||||||
dsrValue = c.value;
|
} else if (c.name === "DSR") {
|
||||||
}
|
dsrValue = c.value;
|
||||||
}
|
}
|
||||||
return {ds: dsValue, dsr: dsrValue};
|
}
|
||||||
}
|
return {ds: dsValue, dsr: dsrValue};
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 从 DS 中解析 email 字段
|
/**
|
||||||
* @param {string} dsToken DS cookie
|
* DS email
|
||||||
* @returns {string|null} 返回 email或null
|
* @param {string} dsToken DS cookie
|
||||||
*/
|
* @returns {string|null} emailnull
|
||||||
function decodeEmailFromDs(dsToken) {
|
*/
|
||||||
try {
|
function decodeEmailFromDs(dsToken) {
|
||||||
const parts = dsToken.split(".");
|
try {
|
||||||
if (parts.length < 2) return null;
|
const parts = dsToken.split(".");
|
||||||
const payload = JSON.parse(Buffer.from(parts[1], "base64").toString("utf-8"));
|
if (parts.length < 2) return null;
|
||||||
return payload.email || null;
|
const payload = JSON.parse(Buffer.from(parts[1], "base64").toString("utf-8"));
|
||||||
} catch (err) {
|
return payload.email || null;
|
||||||
return null;
|
} catch (err) {
|
||||||
}
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
|
||||||
* cookie 数组转换 "name=value; name=value"
|
/**
|
||||||
* @param {Array} cookies
|
* cookie "name=value; name=value"
|
||||||
* @returns {string}
|
* @param {Array} cookies
|
||||||
*/
|
* @returns {string}
|
||||||
function cookiesToStringAll(cookies) {
|
*/
|
||||||
return cookies.map(c => `${c.name}=${c.value}`).join("; ");
|
function cookiesToStringAll(cookies) {
|
||||||
}
|
return cookies.map(c => `${c.name}=${c.value}`).join("; ");
|
||||||
|
}
|
||||||
/**
|
|
||||||
* cookie 转换数组
|
/**
|
||||||
* 每个对象如 { name, value }
|
* cookie
|
||||||
* @param {string} cookieStr
|
* { name, value }
|
||||||
* @returns {Array}
|
* @param {string} cookieStr
|
||||||
*/
|
* @returns {Array}
|
||||||
function parseCookieString(cookieStr) {
|
*/
|
||||||
return cookieStr.split("; ").map(entry => {
|
function parseCookieString(cookieStr) {
|
||||||
const [name, value] = entry.split("=", 2);
|
return cookieStr.split("; ").map(entry => {
|
||||||
return {name, value};
|
const [name, value] = entry.split("=", 2);
|
||||||
});
|
return {name, value};
|
||||||
}
|
});
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 本地 configObj.sessions 查找与指定 email 匹配的 session,
|
/**
|
||||||
* @param {object} configObj 解析后 config
|
* configObj.sessions email session,
|
||||||
* @param {string} email 匹配的邮箱
|
* @param {object} configObj config
|
||||||
* @returns {{ index: number, oldCookie: string, ds: string, dsr: string } | null}
|
* @param {string} email
|
||||||
*/
|
* @returns {{ index: number, oldCookie: string, ds: string, dsr: string } | null}
|
||||||
function findSessionByEmail(configObj, email) {
|
*/
|
||||||
if (!Array.isArray(configObj.sessions)) return null;
|
function findSessionByEmail(configObj, email) {
|
||||||
for (let i = 0; i < configObj.sessions.length; i++) {
|
if (!Array.isArray(configObj.sessions)) return null;
|
||||||
const cookieStr = configObj.sessions[i].cookie || "";
|
for (let i = 0; i < configObj.sessions.length; i++) {
|
||||||
const dsMatch = /DS=([^;\s]+)/.exec(cookieStr);
|
const cookieStr = configObj.sessions[i].cookie || "";
|
||||||
if (!dsMatch) continue;
|
const dsMatch = /DS=([^;\s]+)/.exec(cookieStr);
|
||||||
const dsValue = dsMatch[1];
|
if (!dsMatch) continue;
|
||||||
const dsEmail = decodeEmailFromDs(dsValue);
|
const dsValue = dsMatch[1];
|
||||||
if (dsEmail && dsEmail.toLowerCase() === email.toLowerCase()) {
|
const dsEmail = decodeEmailFromDs(dsValue);
|
||||||
const dsrMatch = /DSR=([^;\s]+)/.exec(cookieStr);
|
if (dsEmail && dsEmail.toLowerCase() === email.toLowerCase()) {
|
||||||
const dsrValue = dsrMatch ? dsrMatch[1] : "";
|
const dsrMatch = /DSR=([^;\s]+)/.exec(cookieStr);
|
||||||
return {
|
const dsrValue = dsrMatch ? dsrMatch[1] : "";
|
||||||
index: i,
|
return {
|
||||||
oldCookie: cookieStr,
|
index: i,
|
||||||
ds: dsValue,
|
oldCookie: cookieStr,
|
||||||
dsr: dsrValue
|
ds: dsValue,
|
||||||
};
|
dsr: dsrValue
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
return null;
|
}
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
/**
|
|
||||||
* config.mjs 中匹配相同 email 的 session,若 DS 或 DSR 有变化,则更新整个 cookie
|
/**
|
||||||
* @param {import('puppeteer-core').Page} page
|
* 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") {
|
export async function updateLocalConfigCookieByEmail(page) {
|
||||||
return;
|
if (!ENABLE_AUTO_COOKIE_UPDATE || process.env.USE_MANUAL_LOGIN === "true") {
|
||||||
}
|
return;
|
||||||
// 尝试从 “https://you.com/api/instrumentation” 获取 cookie
|
}
|
||||||
let cookieStringFromInstrumentation = "";
|
// Try fetching cookie from https://you.com/api/instrumentation
|
||||||
try {
|
let cookieStringFromInstrumentation = "";
|
||||||
const instrRequest = await page.waitForRequest(
|
try {
|
||||||
req => req.url().includes("/api/instrumentation"),
|
const instrRequest = await page.waitForRequest(
|
||||||
{timeout: 5000}
|
req => req.url().includes("/api/instrumentation"),
|
||||||
);
|
{timeout: 5000}
|
||||||
if (instrRequest) {
|
);
|
||||||
cookieStringFromInstrumentation = instrRequest.headers()["cookie"];
|
if (instrRequest) {
|
||||||
}
|
cookieStringFromInstrumentation = instrRequest.headers()["cookie"];
|
||||||
} catch (err) {
|
}
|
||||||
}
|
} catch (err) {
|
||||||
|
}
|
||||||
let allCookiesString = "";
|
|
||||||
if (cookieStringFromInstrumentation) {
|
let allCookiesString = "";
|
||||||
allCookiesString = cookieStringFromInstrumentation;
|
if (cookieStringFromInstrumentation) {
|
||||||
} else {
|
allCookiesString = cookieStringFromInstrumentation;
|
||||||
// 使用 page.cookies() 获取
|
} else {
|
||||||
const cookies = await page.cookies("https://you.com");
|
// Fetch using page.cookies()
|
||||||
allCookiesString = cookiesToStringAll(cookies);
|
const cookies = await page.cookies("https://you.com");
|
||||||
}
|
allCookiesString = cookiesToStringAll(cookies);
|
||||||
|
}
|
||||||
const cookieArray = parseCookieString(allCookiesString);
|
|
||||||
const {ds: newDs, dsr: newDsr} = parseDSAndDSR(cookieArray);
|
const cookieArray = parseCookieString(allCookiesString);
|
||||||
if (!newDs) {
|
const {ds: newDs, dsr: newDsr} = parseDSAndDSR(cookieArray);
|
||||||
console.log("网页未找到 DS,跳过更新。");
|
if (!newDs) {
|
||||||
return;
|
sysLogger.debug("DS not found on page, skipping update.");
|
||||||
}
|
return;
|
||||||
const newEmail = decodeEmailFromDs(newDs);
|
}
|
||||||
if (!newEmail) {
|
const newEmail = decodeEmailFromDs(newDs);
|
||||||
console.log("[网页无法从 DS 解出 email,跳过更新。");
|
if (!newEmail) {
|
||||||
return;
|
sysLogger.debug("[Page cannot resolve email from DS, skipping update.");
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
// 互斥区
|
|
||||||
await configMutex.runExclusive(async () => {
|
// Mutex area
|
||||||
try {
|
await configMutex.runExclusive(async () => {
|
||||||
if (!fs.existsSync(CONFIG_FILE_PATH)) {
|
try {
|
||||||
console.warn(`找不到 config.mjs: ${CONFIG_FILE_PATH}`);
|
if (!fs.existsSync(CONFIG_FILE_PATH)) {
|
||||||
return;
|
sysLogger.warn(`config.mjs not found: ${CONFIG_FILE_PATH}`);
|
||||||
}
|
return;
|
||||||
const raw = fs.readFileSync(CONFIG_FILE_PATH, "utf8");
|
}
|
||||||
// 去掉 export const config =
|
const raw = fs.readFileSync(CONFIG_FILE_PATH, "utf8");
|
||||||
let jsonString = raw.replace(/^export const config\s*=\s*/, "").trim();
|
// Remove export const config =
|
||||||
|
let jsonString = raw.replace(/^export const config\s*=\s*/, "").trim();
|
||||||
jsonString = unifyQuotesForJSON(jsonString);
|
|
||||||
|
jsonString = unifyQuotesForJSON(jsonString);
|
||||||
const configObj = JSON.parse(jsonString);
|
|
||||||
|
const configObj = JSON.parse(jsonString);
|
||||||
const found = findSessionByEmail(configObj, newEmail);
|
|
||||||
if (!found) {
|
const found = findSessionByEmail(configObj, newEmail);
|
||||||
console.log(`未能在 config 中找到 email=${newEmail} 的 session,跳过更新。`);
|
if (!found) {
|
||||||
return;
|
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}),不更新。`);
|
if (found.ds === newDs && found.dsr === newDsr) {
|
||||||
return;
|
sysLogger.debug(`DS/DSR unchanged (email=${newEmail}), skipping update.`);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
configObj.sessions[found.index].cookie = allCookiesString;
|
|
||||||
|
configObj.sessions[found.index].cookie = allCookiesString;
|
||||||
const newFileContent = "export const config = " + JSON.stringify(configObj, null, 4);
|
|
||||||
fs.writeFileSync(CONFIG_FILE_PATH, newFileContent, "utf8");
|
const newFileContent = "export const config = " + JSON.stringify(configObj, null, 4);
|
||||||
|
fs.writeFileSync(CONFIG_FILE_PATH, newFileContent, "utf8");
|
||||||
console.log(`Cookie已更新(email=${newEmail})`);
|
|
||||||
} catch (err) {
|
sysLogger.debug(`Cookie updated (email=${newEmail})`);
|
||||||
console.warn("Cookie更新过程出错:", err);
|
} catch (err) {
|
||||||
}
|
sysLogger.warn("Error during cookie update:", err);
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 非阻塞
|
/**
|
||||||
* @param {import('puppeteer-core').Page} page
|
*
|
||||||
*/
|
* @param {import('puppeteer-core').Page} page
|
||||||
export function updateLocalConfigCookieByEmailNonBlocking(page) {
|
*/
|
||||||
// 保证异步
|
export function updateLocalConfigCookieByEmailNonBlocking(page) {
|
||||||
setImmediate(() => {
|
// Ensure async
|
||||||
updateLocalConfigCookieByEmail(page).catch(err =>
|
setImmediate(() => {
|
||||||
console.error("Cookie update error:", err)
|
updateLocalConfigCookieByEmail(page).catch(err =>
|
||||||
);
|
sysLogger.error("Cookie update error:", err)
|
||||||
});
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -1,49 +1,50 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import sysLogger from '../utils/sysLogger.mjs';
|
||||||
export function getRandomInt(min, max) {
|
|
||||||
min = Math.ceil(min);
|
export function getRandomInt(min, max) {
|
||||||
max = Math.floor(max);
|
min = Math.ceil(min);
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
max = Math.floor(max);
|
||||||
}
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
// 插入乱码
|
|
||||||
export function insertGarbledText(content) {
|
// Insert garbage code
|
||||||
const enableGarbledStart = process.env.ENABLE_GARBLED_START === 'true';
|
export function insertGarbledText(content) {
|
||||||
const enableGarbledEnd = process.env.ENABLE_GARBLED_END === 'true';
|
const enableGarbledStart = process.env.ENABLE_GARBLED_START === 'true';
|
||||||
|
const enableGarbledEnd = process.env.ENABLE_GARBLED_END === 'true';
|
||||||
if (!enableGarbledStart && !enableGarbledEnd) {
|
|
||||||
return content;
|
if (!enableGarbledStart && !enableGarbledEnd) {
|
||||||
}
|
return content;
|
||||||
|
}
|
||||||
// 生成指定长度的随机乱码
|
|
||||||
function generateGarbledText(length) {
|
// Generate random garbage of specified length
|
||||||
return crypto.randomBytes(length).toString('hex');
|
function generateGarbledText(length) {
|
||||||
}
|
return crypto.randomBytes(length).toString('hex');
|
||||||
|
}
|
||||||
let garbledContent = content;
|
|
||||||
|
let garbledContent = content;
|
||||||
// 配置参数
|
|
||||||
const startMinLength = parseInt(process.env.GARBLED_START_MIN_LENGTH) || 1000;
|
// Configuration parameters
|
||||||
const startMaxLength = parseInt(process.env.GARBLED_START_MAX_LENGTH) || 5000;
|
const startMinLength = parseInt(process.env.GARBLED_START_MIN_LENGTH) || 1000;
|
||||||
|
const startMaxLength = parseInt(process.env.GARBLED_START_MAX_LENGTH) || 5000;
|
||||||
const endGarbledLength = parseInt(process.env.GARBLED_END_LENGTH) || 500;
|
|
||||||
|
const endGarbledLength = parseInt(process.env.GARBLED_END_LENGTH) || 500;
|
||||||
if (enableGarbledStart) {
|
|
||||||
const startGarbledLength = getRandomInt(startMinLength, startMaxLength);
|
if (enableGarbledStart) {
|
||||||
|
const startGarbledLength = getRandomInt(startMinLength, startMaxLength);
|
||||||
const byteLength = Math.ceil(startGarbledLength / 2);
|
|
||||||
|
const byteLength = Math.ceil(startGarbledLength / 2);
|
||||||
// 生成乱码
|
|
||||||
const startPlaceholder = generateGarbledText(byteLength);
|
// Generate garbage
|
||||||
|
const startPlaceholder = generateGarbledText(byteLength);
|
||||||
garbledContent = startPlaceholder + '\n\n\n' + garbledContent.trim();
|
|
||||||
}
|
garbledContent = startPlaceholder + '\n\n\n' + garbledContent.trim();
|
||||||
|
}
|
||||||
if (enableGarbledEnd) {
|
|
||||||
const byteLength = Math.ceil(endGarbledLength / 2);
|
if (enableGarbledEnd) {
|
||||||
const endPlaceholder = generateGarbledText(byteLength);
|
const byteLength = Math.ceil(endGarbledLength / 2);
|
||||||
garbledContent = garbledContent.trim() + '\n\n\n' + endPlaceholder;
|
const endPlaceholder = generateGarbledText(byteLength);
|
||||||
}
|
garbledContent = garbledContent.trim() + '\n\n\n' + endPlaceholder;
|
||||||
|
}
|
||||||
return garbledContent;
|
|
||||||
|
return garbledContent;
|
||||||
}
|
}
|
||||||
@@ -1,292 +1,299 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import {fileURLToPath} from 'url';
|
import {fileURLToPath} from 'url';
|
||||||
import {Mutex} from 'async-mutex';
|
import {Mutex} from 'async-mutex';
|
||||||
|
import sysLogger from '../utils/sysLogger.mjs';
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
class Logger {
|
|
||||||
constructor() {
|
class Logger {
|
||||||
this.logMutex = new Mutex();
|
constructor() {
|
||||||
this.logFilePath = path.join(__dirname, 'requests.log');
|
this.logMutex = new Mutex();
|
||||||
this.statistics = {};
|
const dataDir = path.join(process.cwd(), 'data');
|
||||||
this.monthStart = this.getMonthStart();
|
if (!fs.existsSync(dataDir)) {
|
||||||
this.today = this.getToday();
|
fs.mkdirSync(dataDir, { recursive: true });
|
||||||
this.loadStatistics();
|
}
|
||||||
}
|
this.logFilePath = path.join(dataDir, 'requests.log');
|
||||||
|
this.statistics = {};
|
||||||
getMonthStart() {
|
this.monthStart = this.getMonthStart();
|
||||||
const now = new Date();
|
this.today = this.getToday();
|
||||||
// 每月第一天
|
this.loadStatistics();
|
||||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
}
|
||||||
monthStart.setHours(0, 0, 0, 0);
|
|
||||||
return monthStart;
|
getMonthStart() {
|
||||||
}
|
const now = new Date();
|
||||||
|
// First days of month
|
||||||
getToday() {
|
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
const now = new Date();
|
monthStart.setHours(0, 0, 0, 0);
|
||||||
// 获取当天日期
|
return monthStart;
|
||||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
}
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
return today;
|
getToday() {
|
||||||
}
|
const now = new Date();
|
||||||
|
// Get current day date
|
||||||
// 加载日志
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
loadStatistics() {
|
today.setHours(0, 0, 0, 0);
|
||||||
this.logMutex.runExclusive(() => {
|
return today;
|
||||||
if (!fs.existsSync(this.logFilePath)) {
|
}
|
||||||
fs.writeFileSync(this.logFilePath, '', 'utf8');
|
|
||||||
return;
|
// Load log
|
||||||
}
|
async loadStatistics() {
|
||||||
const data = fs.readFileSync(this.logFilePath, 'utf-8');
|
await this.logMutex.runExclusive(async () => {
|
||||||
const entries = data.split('\n').filter(line => line.trim());
|
try {
|
||||||
const validEntries = [];
|
await fs.promises.access(this.logFilePath, fs.constants.F_OK);
|
||||||
|
} catch (err) {
|
||||||
for (const line of entries) {
|
await fs.promises.writeFile(this.logFilePath, '', 'utf8');
|
||||||
try {
|
return;
|
||||||
const logEntry = JSON.parse(line);
|
}
|
||||||
|
const data = await fs.promises.readFile(this.logFilePath, 'utf-8');
|
||||||
// 补全缺少的字段
|
const entries = data.split('\n').filter(line => line.trim());
|
||||||
if (!logEntry.provider) {
|
const validEntries = [];
|
||||||
logEntry.provider = 'you';
|
|
||||||
}
|
for (const line of entries) {
|
||||||
if (!logEntry.email) {
|
try {
|
||||||
logEntry.email = 'unknown';
|
const logEntry = JSON.parse(line);
|
||||||
}
|
|
||||||
if (!logEntry.mode) {
|
// Fill in missing fields
|
||||||
logEntry.mode = 'default';
|
if (!logEntry.provider) {
|
||||||
}
|
logEntry.provider = 'you';
|
||||||
if (logEntry.model === undefined) {
|
}
|
||||||
logEntry.model = 'unknown';
|
if (!logEntry.email) {
|
||||||
}
|
logEntry.email = 'unknown';
|
||||||
if (logEntry.completed === undefined) {
|
}
|
||||||
logEntry.completed = false;
|
if (!logEntry.mode) {
|
||||||
}
|
logEntry.mode = 'default';
|
||||||
if (logEntry.unusualQueryVolume === undefined) {
|
}
|
||||||
logEntry.unusualQueryVolume = false;
|
if (logEntry.model === undefined) {
|
||||||
}
|
logEntry.model = 'unknown';
|
||||||
|
}
|
||||||
// 调整字段顺序
|
if (logEntry.completed === undefined) {
|
||||||
const logEntryArray = [
|
logEntry.completed = false;
|
||||||
['provider', logEntry.provider],
|
}
|
||||||
['email', logEntry.email],
|
if (logEntry.unusualQueryVolume === undefined) {
|
||||||
['time', logEntry.time],
|
logEntry.unusualQueryVolume = false;
|
||||||
['mode', logEntry.mode],
|
}
|
||||||
['model', logEntry.model],
|
|
||||||
['completed', logEntry.completed],
|
// Adjust field order
|
||||||
['unusualQueryVolume', logEntry.unusualQueryVolume],
|
const logEntryArray = [
|
||||||
];
|
['provider', logEntry.provider],
|
||||||
const formattedLogEntry = Object.fromEntries(logEntryArray);
|
['email', logEntry.email],
|
||||||
|
['time', logEntry.time],
|
||||||
validEntries.push(formattedLogEntry);
|
['mode', logEntry.mode],
|
||||||
} catch (e) {
|
['model', logEntry.model],
|
||||||
console.warn(`无法解析的日志,已忽略: ${line}`);
|
['completed', logEntry.completed],
|
||||||
}
|
['unusualQueryVolume', logEntry.unusualQueryVolume],
|
||||||
}
|
];
|
||||||
|
const formattedLogEntry = Object.fromEntries(logEntryArray);
|
||||||
// 处理有效日志
|
|
||||||
for (const logEntry of validEntries) {
|
validEntries.push(formattedLogEntry);
|
||||||
const logDate = new Date(logEntry.time);
|
} catch (e) {
|
||||||
const provider = logEntry.provider;
|
sysLogger.warn(`Unparseable log, ignored: ${line}`);
|
||||||
const email = logEntry.email;
|
}
|
||||||
|
}
|
||||||
// 初始化 provider
|
|
||||||
if (!this.statistics[provider]) {
|
// Process valid log
|
||||||
this.statistics[provider] = {};
|
for (const logEntry of validEntries) {
|
||||||
}
|
const logDate = new Date(logEntry.time);
|
||||||
|
const provider = logEntry.provider;
|
||||||
// 初始化邮箱
|
const email = logEntry.email;
|
||||||
if (!this.statistics[provider][email]) {
|
|
||||||
this.statistics[provider][email] = {
|
// Initialize provider
|
||||||
allRequests: [], // 所有请求
|
if (!this.statistics[provider]) {
|
||||||
monthlyRequests: [], // 本月请求
|
this.statistics[provider] = {};
|
||||||
dailyRequests: [], // 当日请求
|
}
|
||||||
monthlyStats: {
|
|
||||||
totalRequests: 0,
|
// Initialize email
|
||||||
defaultModeCount: 0,
|
if (!this.statistics[provider][email]) {
|
||||||
customModeCount: 0,
|
this.statistics[provider][email] = {
|
||||||
modelCount: {},
|
allRequests: [], // All requests
|
||||||
},
|
monthlyRequests: [], // Requests this month
|
||||||
dailyStats: {
|
dailyRequests: [], // Today's requests
|
||||||
totalRequests: 0,
|
monthlyStats: {
|
||||||
defaultModeCount: 0,
|
totalRequests: 0,
|
||||||
customModeCount: 0,
|
defaultModeCount: 0,
|
||||||
modelCount: {},
|
customModeCount: 0,
|
||||||
}
|
modelCount: {},
|
||||||
};
|
},
|
||||||
}
|
dailyStats: {
|
||||||
|
totalRequests: 0,
|
||||||
const stats = this.statistics[provider][email];
|
defaultModeCount: 0,
|
||||||
stats.allRequests.push(logEntry);
|
customModeCount: 0,
|
||||||
|
modelCount: {},
|
||||||
// 本月统计
|
}
|
||||||
if (logDate >= this.monthStart) {
|
};
|
||||||
stats.monthlyRequests.push(logEntry);
|
}
|
||||||
this.updateStatistics(stats.monthlyStats, logEntry);
|
|
||||||
}
|
const stats = this.statistics[provider][email];
|
||||||
|
stats.allRequests.push(logEntry);
|
||||||
// 当日统计
|
|
||||||
if (logDate >= this.today) {
|
// This month's statistics
|
||||||
stats.dailyRequests.push(logEntry);
|
if (logDate >= this.monthStart) {
|
||||||
this.updateStatistics(stats.dailyStats, logEntry);
|
stats.monthlyRequests.push(logEntry);
|
||||||
}
|
this.updateStatistics(stats.monthlyStats, logEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对每个 provider 的每个邮箱时间排序
|
// Today's statistics
|
||||||
for (const provider in this.statistics) {
|
if (logDate >= this.today) {
|
||||||
for (const email in this.statistics[provider]) {
|
stats.dailyRequests.push(logEntry);
|
||||||
const stats = this.statistics[provider][email];
|
this.updateStatistics(stats.dailyStats, logEntry);
|
||||||
stats.allRequests.sort((a, b) => new Date(b.time) - new Date(a.time));
|
}
|
||||||
stats.monthlyRequests.sort((a, b) => new Date(b.time) - new Date(a.time));
|
}
|
||||||
stats.dailyRequests.sort((a, b) => new Date(b.time) - new Date(a.time));
|
|
||||||
}
|
// 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];
|
||||||
const cleanedData = validEntries.map(entry => JSON.stringify(entry)).join('\n') + '\n';
|
stats.allRequests.sort((a, b) => new Date(b.time) - new Date(a.time));
|
||||||
fs.writeFileSync(this.logFilePath, cleanedData);
|
stats.monthlyRequests.sort((a, b) => new Date(b.time) - new Date(a.time));
|
||||||
}).catch(err => {
|
stats.dailyRequests.sort((a, b) => new Date(b.time) - new Date(a.time));
|
||||||
console.error('loadStatistics() 加锁异常:', err);
|
}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
// Clean invalid data
|
||||||
// 更新统计
|
const cleanedData = validEntries.map(entry => JSON.stringify(entry)).join('\n') + '\n';
|
||||||
updateStatistics(stats, logEntry) {
|
await fs.promises.writeFile(this.logFilePath, cleanedData);
|
||||||
stats.totalRequests++;
|
}).catch(err => {
|
||||||
if (logEntry.mode === 'default') {
|
sysLogger.error('loadStatistics() lock exception:', err);
|
||||||
stats.defaultModeCount++;
|
});
|
||||||
} else if (logEntry.mode === 'custom') {
|
}
|
||||||
stats.customModeCount++;
|
|
||||||
}
|
// Update statistics
|
||||||
|
updateStatistics(stats, logEntry) {
|
||||||
if (logEntry.model) {
|
stats.totalRequests++;
|
||||||
if (!stats.modelCount[logEntry.model]) {
|
if (logEntry.mode === 'default') {
|
||||||
stats.modelCount[logEntry.model] = 0;
|
stats.defaultModeCount++;
|
||||||
}
|
} else if (logEntry.mode === 'custom') {
|
||||||
stats.modelCount[logEntry.model]++;
|
stats.customModeCount++;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (logEntry.model) {
|
||||||
// 记录请求日志
|
if (!stats.modelCount[logEntry.model]) {
|
||||||
logRequest({provider, email, time, mode, model, completed, unusualQueryVolume}) {
|
stats.modelCount[logEntry.model] = 0;
|
||||||
const logEntryArray = [
|
}
|
||||||
['provider', provider || process.env.ACTIVE_PROVIDER || 'you'],
|
stats.modelCount[logEntry.model]++;
|
||||||
['email', email || 'unknown'],
|
}
|
||||||
['time', time],
|
}
|
||||||
['mode', mode || 'unknown'],
|
|
||||||
['model', model || 'unknown'],
|
// Record request log
|
||||||
['completed', completed || 'unknown'],
|
logRequest({provider, email, time, mode, model, completed, unusualQueryVolume}) {
|
||||||
['unusualQueryVolume', unusualQueryVolume || 'unknown'],
|
const logEntryArray = [
|
||||||
];
|
['provider', provider || 'you'],
|
||||||
const logEntry = Object.fromEntries(logEntryArray);
|
['email', email || 'unknown'],
|
||||||
|
['time', time],
|
||||||
// 写日志与更新 statistics
|
['mode', mode || 'unknown'],
|
||||||
this.logMutex.runExclusive(() => {
|
['model', model || 'unknown'],
|
||||||
fs.appendFileSync(this.logFilePath, JSON.stringify(logEntry) + '\n');
|
['completed', completed || 'unknown'],
|
||||||
|
['unusualQueryVolume', unusualQueryVolume || 'unknown'],
|
||||||
const logDate = new Date(logEntry.time);
|
];
|
||||||
const providerName = logEntry.provider;
|
const logEntry = Object.fromEntries(logEntryArray);
|
||||||
if (!this.statistics[providerName]) {
|
|
||||||
this.statistics[providerName] = {};
|
// Write log and update statistics
|
||||||
}
|
this.logMutex.runExclusive(async () => {
|
||||||
|
await fs.promises.appendFile(this.logFilePath, JSON.stringify(logEntry) + '\n');
|
||||||
const userEmail = logEntry.email;
|
|
||||||
if (!this.statistics[providerName][userEmail]) {
|
const logDate = new Date(logEntry.time);
|
||||||
this.statistics[providerName][userEmail] = {
|
const providerName = logEntry.provider;
|
||||||
allRequests: [],
|
if (!this.statistics[providerName]) {
|
||||||
monthlyRequests: [],
|
this.statistics[providerName] = {};
|
||||||
dailyRequests: [],
|
}
|
||||||
monthlyStats: {
|
|
||||||
totalRequests: 0,
|
const userEmail = logEntry.email;
|
||||||
defaultModeCount: 0,
|
if (!this.statistics[providerName][userEmail]) {
|
||||||
customModeCount: 0,
|
this.statistics[providerName][userEmail] = {
|
||||||
modelCount: {},
|
allRequests: [],
|
||||||
},
|
monthlyRequests: [],
|
||||||
dailyStats: {
|
dailyRequests: [],
|
||||||
totalRequests: 0,
|
monthlyStats: {
|
||||||
defaultModeCount: 0,
|
totalRequests: 0,
|
||||||
customModeCount: 0,
|
defaultModeCount: 0,
|
||||||
modelCount: {},
|
customModeCount: 0,
|
||||||
}
|
modelCount: {},
|
||||||
};
|
},
|
||||||
}
|
dailyStats: {
|
||||||
|
totalRequests: 0,
|
||||||
const stats = this.statistics[providerName][userEmail];
|
defaultModeCount: 0,
|
||||||
stats.allRequests.push(logEntry);
|
customModeCount: 0,
|
||||||
|
modelCount: {},
|
||||||
// 当日统计
|
}
|
||||||
if (logDate >= this.today) {
|
};
|
||||||
stats.dailyRequests.push(logEntry);
|
}
|
||||||
this.updateStatistics(stats.dailyStats, logEntry);
|
|
||||||
}
|
const stats = this.statistics[providerName][userEmail];
|
||||||
|
stats.allRequests.push(logEntry);
|
||||||
// 本月统计
|
|
||||||
if (logDate >= this.monthStart) {
|
// Today's statistics
|
||||||
stats.monthlyRequests.push(logEntry);
|
if (logDate >= this.today) {
|
||||||
this.updateStatistics(stats.monthlyStats, logEntry);
|
stats.dailyRequests.push(logEntry);
|
||||||
}
|
this.updateStatistics(stats.dailyStats, logEntry);
|
||||||
}).catch(err => {
|
}
|
||||||
console.error('logRequest() 加锁异常:', err);
|
|
||||||
});
|
// This month's statistics
|
||||||
}
|
if (logDate >= this.monthStart) {
|
||||||
|
stats.monthlyRequests.push(logEntry);
|
||||||
// 输出当前统计信息
|
this.updateStatistics(stats.monthlyStats, logEntry);
|
||||||
printStatistics() {
|
}
|
||||||
const provider = process.env.ACTIVE_PROVIDER || 'you';
|
}).catch(err => {
|
||||||
const monthStartStr = this.monthStart.toLocaleDateString('zh-CN', {
|
sysLogger.error('logRequest() lock exception:', err);
|
||||||
year: 'numeric',
|
});
|
||||||
month: 'long',
|
}
|
||||||
day: 'numeric'
|
|
||||||
});
|
// Output current statistics
|
||||||
const todayStr = this.today.toLocaleDateString('zh-CN', {
|
printStatistics() {
|
||||||
year: 'numeric',
|
const provider = 'you';
|
||||||
month: 'long',
|
const monthStartStr = this.monthStart.toLocaleDateString('zh-CN', {
|
||||||
day: 'numeric'
|
year: 'numeric',
|
||||||
});
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
if (!this.statistics[provider]) {
|
});
|
||||||
console.log(`===== 提供者 ${provider} 没有统计数据 =====`);
|
const todayStr = this.today.toLocaleDateString('zh-CN', {
|
||||||
return;
|
year: 'numeric',
|
||||||
}
|
month: 'long',
|
||||||
const emails = Object.keys(this.statistics[provider]).sort();
|
day: 'numeric'
|
||||||
let hasAnyDailyRequest = false;
|
});
|
||||||
|
|
||||||
console.log(`===== 请求统计信息 (Provider=${provider}) =====`);
|
if (!this.statistics[provider]) {
|
||||||
|
sysLogger.info(`===== Provider ${provider} has no statistics =====`);
|
||||||
for (const email of emails) {
|
return;
|
||||||
const stats = this.statistics[provider][email];
|
}
|
||||||
// 当日是否有请求
|
const emails = Object.keys(this.statistics[provider]).sort();
|
||||||
if (stats.dailyStats.totalRequests > 0) {
|
let hasAnyDailyRequest = false;
|
||||||
hasAnyDailyRequest = true;
|
|
||||||
console.log(`用户邮箱: ${email}`);
|
sysLogger.info(`===== Request Statistics (Provider=${provider}) =====`);
|
||||||
console.log(`---------- 本月[自 ${monthStartStr} 起] 统计 ----------`);
|
|
||||||
console.log(`总请求次数: ${stats.monthlyStats.totalRequests}`);
|
for (const email of emails) {
|
||||||
console.log(`default 请求次数: ${stats.monthlyStats.defaultModeCount}`);
|
const stats = this.statistics[provider][email];
|
||||||
console.log(`custom 请求次数: ${stats.monthlyStats.customModeCount}`);
|
// Any requests today
|
||||||
console.log('各模型请求次数:');
|
if (stats.dailyStats.totalRequests > 0) {
|
||||||
for (const [mdl, count] of Object.entries(stats.monthlyStats.modelCount)) {
|
hasAnyDailyRequest = true;
|
||||||
console.log(` - ${mdl}: ${count}`);
|
sysLogger.debug(`User email: ${email}`);
|
||||||
}
|
sysLogger.info(`---------- This Month [Since ${monthStartStr} ] Statistics ----------`);
|
||||||
|
sysLogger.info(`Total requests: ${stats.monthlyStats.totalRequests}`);
|
||||||
console.log(`---------- 今日[${todayStr}]统计 ----------`);
|
sysLogger.info(`Default requests: ${stats.monthlyStats.defaultModeCount}`);
|
||||||
console.log(`总请求次数: ${stats.dailyStats.totalRequests}`);
|
sysLogger.info(`Custom requests: ${stats.monthlyStats.customModeCount}`);
|
||||||
console.log(`default 请求次数: ${stats.dailyStats.defaultModeCount}`);
|
sysLogger.info('Requests by model:');
|
||||||
console.log(`custom 请求次数: ${stats.dailyStats.customModeCount}`);
|
for (const [mdl, count] of Object.entries(stats.monthlyStats.modelCount)) {
|
||||||
console.log('各模型请求次数:');
|
sysLogger.debug(` - ${mdl}: ${count}`);
|
||||||
for (const [mdl, count] of Object.entries(stats.dailyStats.modelCount)) {
|
}
|
||||||
console.log(` - ${mdl}: ${count}`);
|
|
||||||
}
|
sysLogger.info(`---------- Today [${todayStr}] Statistics ----------`);
|
||||||
console.log('------------------------------');
|
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:');
|
||||||
if (!hasAnyDailyRequest) {
|
for (const [mdl, count] of Object.entries(stats.dailyStats.modelCount)) {
|
||||||
console.log(`===== 今日(${todayStr})无任何账号发生请求 =====`);
|
sysLogger.debug(` - ${mdl}: ${count}`);
|
||||||
}
|
}
|
||||||
|
sysLogger.debug('------------------------------');
|
||||||
console.log('================================');
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (!hasAnyDailyRequest) {
|
||||||
|
sysLogger.info(`===== No account requests today (${todayStr}) =====`);
|
||||||
|
}
|
||||||
|
|
||||||
|
sysLogger.debug('================================');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default Logger;
|
export default Logger;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,119 +1,119 @@
|
|||||||
@echo off
|
@echo off
|
||||||
|
|
||||||
REM 安装依赖包
|
REM Install dependencies
|
||||||
call npm install
|
call npm install
|
||||||
|
|
||||||
REM 设置代理的网站:you、perplexity、happyapi
|
REM Set the website of the proxy: you, perplexity, happyapi
|
||||||
set ACTIVE_PROVIDER=you
|
set ACTIVE_PROVIDER=you
|
||||||
|
|
||||||
REM 设置指定浏览器,可以是 'chrome', 'edge' 或 'auto'
|
REM Set the browser type, can be 'chrome', 'edge', or 'auto'
|
||||||
set BROWSER_TYPE=auto
|
set BROWSER_TYPE=auto
|
||||||
|
|
||||||
REM 设置是否启用手动登录
|
REM Set whether to enable manual login
|
||||||
set USE_MANUAL_LOGIN=true
|
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
|
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
|
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
|
set SESSION_LOCK_TIMEOUT=180
|
||||||
|
|
||||||
REM 设置是否启用并发限制
|
REM Set whether to enable detection
|
||||||
set ENABLE_DETECTION=true
|
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
|
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
|
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
|
set ENABLE_REQUEST_LIMIT=false
|
||||||
|
|
||||||
REM 是否允许非Pro账户
|
REM Set whether to allow non-Pro accounts
|
||||||
set ALLOW_NON_PRO=false
|
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>"
|
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
|
set ENABLE_DELAY_LOGIC=false
|
||||||
|
|
||||||
REM 设置是否启用隧道访问
|
REM Set whether to enable tunnel access
|
||||||
set ENABLE_TUNNEL=false
|
set ENABLE_TUNNEL=false
|
||||||
|
|
||||||
REM 设置隧道类型 (localtunnel 或 ngrok)
|
REM Set the tunnel type (localtunnel or ngrok)
|
||||||
set TUNNEL_TYPE=ngrok
|
set TUNNEL_TYPE=ngrok
|
||||||
|
|
||||||
REM 设置localtunnel子域名(留空则为随机域名)
|
REM Set the localtunnel subdomain (leave blank for a random domain)
|
||||||
set SUBDOMAIN=
|
set SUBDOMAIN=
|
||||||
|
|
||||||
REM 设置 ngrok AUTH TOKEN
|
REM Set the ngrok authentication token
|
||||||
REM 这是 ngrok 账户的身份验证令牌。可以在 ngrok 仪表板的 "Auth" 部分找到它。
|
REM This is the authentication token for the ngrok account, which can be found in the "Auth" section of the ngrok dashboard.
|
||||||
REM 免费账户和付费账户都需要设置此项。
|
REM Both free and paid accounts require this to be set.
|
||||||
REM ngrok网站: https://dashboard.ngrok.com
|
REM ngrok website: https://dashboard.ngrok.com
|
||||||
set NGROK_AUTH_TOKEN=
|
set NGROK_AUTH_TOKEN=
|
||||||
|
|
||||||
REM 设置 ngrok 自定义域名
|
REM Set the ngrok custom domain
|
||||||
REM 这允许使用自己的域名而不是 ngrok 的随机子域名。
|
REM This allows you to use your own domain instead of ngrok's random subdomain.
|
||||||
REM 注意:此功能仅适用于 ngrok 付费账户。
|
REM Note: This feature is only available for ngrok paid accounts.
|
||||||
REM 使用此功能前,请确保已在 ngrok 仪表板中添加并验证了该域名。
|
REM Before using this feature, make sure you have added and verified the domain in the ngrok dashboard.
|
||||||
REM 格式示例:your-custom-domain.com
|
REM Example format: your-custom-domain.com
|
||||||
REM 如果使用免费账户或不想使用自定义域名,请将此项留空。
|
REM If using a free account or not using a custom domain, leave this blank.
|
||||||
set NGROK_CUSTOM_DOMAIN=
|
set NGROK_CUSTOM_DOMAIN=
|
||||||
|
|
||||||
REM 设置 https_proxy 代理,可以使用本地的socks5或http(s)代理
|
REM Set the https_proxy proxy, can use local socks5 or http(s) proxy
|
||||||
REM 例如,使用 HTTP 代理:export https_proxy=http://127.0.0.1:7890
|
REM For example, using an HTTP proxy: export https_proxy=http://127.0.0.1:7890
|
||||||
REM 或者使用 SOCKS5 代理:export https_proxy=socks5://host:port:username:password
|
REM Or using a SOCKS5 proxy: export https_proxy=socks5://host:port:username:password
|
||||||
set https_proxy=
|
set https_proxy=
|
||||||
|
|
||||||
REM 设置 PASSWORD API密码
|
REM Set the PASSWORD API password
|
||||||
set PASSWORD=
|
set PASSWORD=
|
||||||
|
|
||||||
REM 设置 PORT 端口
|
REM Set the PORT port
|
||||||
set PORT=8080
|
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=
|
set AI_MODEL=
|
||||||
|
|
||||||
REM 自定义会话模式
|
REM Set custom session mode
|
||||||
set USE_CUSTOM_MODE=false
|
set USE_CUSTOM_MODE=false
|
||||||
|
|
||||||
REM 启用模式轮换
|
REM Enable mode rotation
|
||||||
REM 只有当 USE_CUSTOM_MODE 和 ENABLE_MODE_ROTATION 都设置为 true 时,才会启用模式轮换功能。
|
REM Mode rotation is only enabled when both `USE_CUSTOM_MODE` and `ENABLE_MODE_ROTATION` are set to true.
|
||||||
REM 可以在自定义模式和默认模式之间动态切换
|
REM Can dynamically switch between custom mode and default mode
|
||||||
set ENABLE_MODE_ROTATION=false
|
set ENABLE_MODE_ROTATION=false
|
||||||
|
|
||||||
REM 是否启用隐身模式
|
REM Set whether to enable incognito mode
|
||||||
set INCOGNITO_MODE=false
|
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
|
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
|
set UPLOAD_FILE_FORMAT=txt
|
||||||
|
|
||||||
REM 设置是否启用 CLEWD 后处理
|
REM Set whether to enable CLEWD post-processing
|
||||||
set CLEWD_ENABLED=false
|
set CLEWD_ENABLED=false
|
||||||
|
|
||||||
REM ---------------------------------------------------
|
REM ---------------------------------------------------
|
||||||
REM 控制是否在开头插入乱码
|
REM Set whether to insert garbled characters at the beginning
|
||||||
set ENABLE_GARBLED_START=false
|
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
|
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
|
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
|
set GARBLED_END_LENGTH=500
|
||||||
REM 控制是否在结尾插入乱码
|
REM Set whether to insert garbled characters at the end
|
||||||
set ENABLE_GARBLED_END=false
|
set ENABLE_GARBLED_END=false
|
||||||
REM ---------------------------------------------------
|
REM ---------------------------------------------------
|
||||||
|
|
||||||
REM 运行 Node.js 应用程序
|
REM Run the Node.js application
|
||||||
node index.mjs
|
node src/index.mjs
|
||||||
|
|
||||||
REM 暂停脚本执行,等待用户按任意键退出
|
REM Pause script execution, waiting for the user to press any key to exit
|
||||||
pause
|
pause
|
||||||
|
|||||||
@@ -1,112 +1,112 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# 安装依赖包
|
# Install dependencies
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# 设置代理的网站:you、perplexity、happyapi
|
# Set proxy provider: you, perplexity, happyapi
|
||||||
export ACTIVE_PROVIDER=you
|
export ACTIVE_PROVIDER=you
|
||||||
|
|
||||||
# 设置指定浏览器,可以是 'chrome', 'edge' 或 'auto'
|
# Set specified browser, can be 'chrome', 'edge' or 'auto'
|
||||||
export BROWSER_TYPE=auto
|
export BROWSER_TYPE=auto
|
||||||
|
|
||||||
# 设置是否启用手动登录
|
# Set whether to enable manual login
|
||||||
export USE_MANUAL_LOGIN=true
|
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
|
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
|
export BROWSER_INSTANCE_COUNT=1
|
||||||
|
|
||||||
# 设置会话自动释放时间(单位:秒) (0=禁用自动释放)
|
# Set session auto-release time (unit: seconds) (0=disable auto-release)
|
||||||
export SESSION_LOCK_TIMEOUT=180
|
export SESSION_LOCK_TIMEOUT=180
|
||||||
|
|
||||||
# 设置是否启用并发限制
|
# Set whether to enable concurrency limit
|
||||||
export ENABLE_DETECTION=true
|
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
|
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
|
export SKIP_ACCOUNT_VALIDATION=false
|
||||||
|
|
||||||
# 开启请求次数上限(默认限制3次请求) (用于免费账户)
|
# Enable request count limit (default limit 3 requests) (for free accounts)
|
||||||
export ENABLE_REQUEST_LIMIT=false
|
export ENABLE_REQUEST_LIMIT=false
|
||||||
|
|
||||||
# 是否允许非Pro账户
|
# Whether to allow non-Pro accounts
|
||||||
export ALLOW_NON_PRO=false
|
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>"
|
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
|
export ENABLE_DELAY_LOGIC=false
|
||||||
|
|
||||||
# 设置是否启用隧道访问
|
# Set whether to enable tunnel access
|
||||||
export ENABLE_TUNNEL=false
|
export ENABLE_TUNNEL=false
|
||||||
|
|
||||||
# 设置隧道类型 (localtunnel 或 ngrok)
|
# Set tunnel type (localtunnel or ngrok)
|
||||||
export TUNNEL_TYPE=ngrok
|
export TUNNEL_TYPE=ngrok
|
||||||
|
|
||||||
# 设置localtunnel子域名(留空则为随机域名)
|
# Set localtunnel subdomain (leave empty for random domain)
|
||||||
export SUBDOMAIN=
|
export SUBDOMAIN=
|
||||||
|
|
||||||
# 设置 ngrok AUTH TOKEN
|
# Set ngrok AUTH TOKEN
|
||||||
# 这是 ngrok 账户的身份验证令牌。可以在 ngrok 仪表板的 "Auth" 部分找到它。
|
# 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网站: https://dashboard.ngrok.com
|
# ngrok website: https://dashboard.ngrok.com
|
||||||
export NGROK_AUTH_TOKEN=
|
export NGROK_AUTH_TOKEN=
|
||||||
|
|
||||||
# 设置 ngrok 自定义域名
|
# Set ngrok custom domain
|
||||||
# 这允许使用自己的域名而不是 ngrok 的随机子域名。
|
# This allows using your own domain instead of ngrok's random subdomain.
|
||||||
# 注意:此功能仅适用于 ngrok 付费账户。
|
# Note: This feature only works with ngrok paid accounts.
|
||||||
# 使用此功能前,请确保已在 ngrok 仪表板中添加并验证了该域名。
|
# Before using this feature, ensure you have added and verified the domain in the ngrok dashboard.
|
||||||
# 格式示例:your-custom-domain.com
|
# 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=
|
export NGROK_CUSTOM_DOMAIN=
|
||||||
|
|
||||||
# 设置 https_proxy 代理,可以使用本地的socks5或http(s)代理
|
# Set https_proxy proxy, can use local socks5 or http(s) proxy
|
||||||
# 例如,使用 HTTP 代理:export https_proxy=http://127.0.0.1:7890
|
# For example, using HTTP proxy: export https_proxy=http://127.0.0.1:7890
|
||||||
# 或者使用 SOCKS5 代理:export https_proxy=socks5://host:port:username:password
|
# Or using SOCKS5 proxy: export https_proxy=socks5://host:port:username:password
|
||||||
export https_proxy=
|
export https_proxy=
|
||||||
|
|
||||||
# 设置 PASSWORD API密码
|
# Set PASSWORD API password
|
||||||
export PASSWORD=
|
export PASSWORD=
|
||||||
|
|
||||||
# 设置 PORT 端口
|
# Set PORT port
|
||||||
export PORT=8080
|
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=
|
export AI_MODEL=
|
||||||
|
|
||||||
# 自定义会话模式
|
# Custom session mode
|
||||||
export USE_CUSTOM_MODE=false
|
export USE_CUSTOM_MODE=false
|
||||||
|
|
||||||
# 启用模式轮换
|
# Enable mode rotation
|
||||||
# 只有当 USE_CUSTOM_MODE 和 ENABLE_MODE_ROTATION 都设置为 true 时,才会启用模式轮换功能。
|
# 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
|
export ENABLE_MODE_ROTATION=false
|
||||||
|
|
||||||
# 是否启用隐身模式
|
# Whether to enable incognito mode
|
||||||
export INCOGNITO_MODE=false
|
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
|
export UPLOAD_FILE_FORMAT=docx
|
||||||
|
|
||||||
# ---------------------------------------------------
|
# ---------------------------------------------------
|
||||||
# 控制是否在开头插入乱码
|
# Control whether to insert garbled text at the beginning
|
||||||
export ENABLE_GARBLED_START=false
|
export ENABLE_GARBLED_START=false
|
||||||
# 设置开头插入乱码最小长度
|
# Set minimum length of garbled text at the beginning
|
||||||
export GARBLED_START_MIN_LENGTH=1000
|
export GARBLED_START_MIN_LENGTH=1000
|
||||||
# 设置开头插入乱码最大长度
|
# Set maximum length of garbled text at the beginning
|
||||||
export GARBLED_START_MAX_LENGTH=5000
|
export GARBLED_START_MAX_LENGTH=5000
|
||||||
# 设置结尾插入乱码固定长度
|
# Set fixed length of garbled text at the end
|
||||||
export GARBLED_END_LENGTH=500
|
export GARBLED_END_LENGTH=500
|
||||||
# 控制是否在结尾插入乱码
|
# Control whether to insert garbled text at the end
|
||||||
export ENABLE_GARBLED_END=false
|
export ENABLE_GARBLED_END=false
|
||||||
# ---------------------------------------------------
|
# ---------------------------------------------------
|
||||||
|
|
||||||
# 运行 Node.js 应用程序
|
# Run Node.js application
|
||||||
node index.mjs
|
node src/index.mjs
|
||||||
|
|
||||||
read -p "Press any key to exit..."
|
read -p "Press any key to exit..."
|
||||||
|
|||||||
@@ -1,588 +1,291 @@
|
|||||||
# 使用指南 / Usage Guide
|
# Usage Guide
|
||||||
|
|
||||||
## 前提条件 / Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
1. **安装必要的软件:**
|
1. **Install necessary software:**
|
||||||
|
|
||||||
- Node.js
|
- Node.js
|
||||||
- Git
|
- Git
|
||||||
- Python
|
- Python
|
||||||
|
- Visual C++ Build Tools ([Download link](https://visualstudio.microsoft.com/visual-cpp-build-tools/))
|
||||||
2. **获得一个 You.com 账户并订阅 Pro 或 Team 计划,登录账户。**
|
|
||||||
|
2. **Obtain a You.com account and subscribe to Pro or Team plan, then log in.**
|
||||||
3. **建议全局代理来确保网络连接稳定。**
|
|
||||||
|
3. **Global proxy is recommended to ensure stable network connection.**
|
||||||
4. **如果需要,可以在 `start.bat` 文件中设置代理。**
|
|
||||||
|
4. **If needed, you can set up a proxy in the `start.bat` file.**
|
||||||
---
|
|
||||||
|
---
|
||||||
## 设置步骤 / Setup Steps
|
|
||||||
|
## Setup Steps
|
||||||
### 方法一:使用 Cookie 登录(默认情况下)
|
|
||||||
|
### Method 1: Login using Cookie (Default)
|
||||||
#### 步骤 1:获取 Cookie
|
|
||||||
|
#### Step 1: Obtain Cookie
|
||||||
1. **打开浏览器,登录 [you.com](https://you.com)。**
|
|
||||||
|
1. **Open browser and log in to [you.com](https://you.com).**
|
||||||
2. **按 `F12` 打开开发者工具,找到 "Console"(控制台)选项卡。**
|
|
||||||
|
2. **Press `F12` to open developer tools, find the "Console" tab.**
|
||||||
3. **在控制台中输入以下代码并回车,然后复制所有输出内容(Cookie):**
|
|
||||||
|
3. **Enter the following code in the console and press enter, then copy all output content (Cookie):**
|
||||||
```javascript
|
|
||||||
console.log(document.cookie);
|
```javascript
|
||||||
```
|
console.log(document.cookie);
|
||||||
|
```
|
||||||
#### 步骤 2:配置项目
|
|
||||||
|
#### Step 2: Configure Project
|
||||||
1. **下载或克隆本项目代码,解压缩。**
|
|
||||||
|
1. **Download or clone this project code, unzip.**
|
||||||
2. **编辑 `config.example.mjs` 文件,将上一步获取的 Cookie 粘贴进去。**
|
|
||||||
|
2. **Edit `config.example.mjs` file, paste the Cookie obtained in the previous step.**
|
||||||
如果有多个 Cookie,按照以下格式添加,然后将文件另存为 `config.mjs`:
|
|
||||||
|
If there are multiple Cookies, add them in the following format, then save the file as `config.mjs`:
|
||||||
```javascript
|
|
||||||
export const config = {
|
```javascript
|
||||||
"sessions": [
|
export const config = {
|
||||||
{
|
"sessions": [
|
||||||
"cookie": `cookie1`
|
{
|
||||||
},
|
"cookie": `cookie1`
|
||||||
{
|
},
|
||||||
"cookie": `cookie2`
|
{
|
||||||
},
|
"cookie": `cookie2`
|
||||||
{
|
},
|
||||||
"cookie": `cookie3`
|
{
|
||||||
}
|
"cookie": `cookie3`
|
||||||
]
|
}
|
||||||
}
|
]
|
||||||
```
|
}
|
||||||
|
```
|
||||||
#### 步骤 3:配置环境变量
|
|
||||||
|
#### Step 3: Configure Environment Variables
|
||||||
1. **打开 `start.bat` 文件,根据需要设置环境变量。**
|
|
||||||
|
1. **Open `start.bat` file, set environment variables as needed.**
|
||||||
#### 步骤 4:启动服务
|
|
||||||
|
#### Step 4: Start Service
|
||||||
1. **双击运行 `start.bat`。**
|
|
||||||
|
1. **Double-click to run `start.bat`.**
|
||||||
2. **等待程序安装依赖并启动服务。**
|
|
||||||
|
2. **Wait for the program to install dependencies and start the service.**
|
||||||
#### 步骤 5:配置客户端
|
|
||||||
|
#### Step 5: Configure Client
|
||||||
1. **在 SillyTavern 中选择 **Custom (OpenAI-compatible)**。**
|
|
||||||
|
1. **In SillyTavern, select **Custom (OpenAI-compatible)**.**
|
||||||
2. **将反向代理地址设置为 `http://127.0.0.1:8080/v1`。**
|
|
||||||
|
2. **Set the reverse proxy address to `http://127.0.0.1:8080/v1`.**
|
||||||
3. **反代密码需要填写(随便填一个即可,除非在 `start.bat` 中设置了 `PASSWORD`)。**
|
|
||||||
|
3. **Reverse proxy password needs to be filled (any value will do, unless `PASSWORD` is set in `start.bat`).**
|
||||||
4. **开始使用。如果失败或没有结果,尝试多次重试。**
|
|
||||||
|
4. **Start using. If it fails or there's no result, try multiple retries.**
|
||||||
---
|
|
||||||
|
---
|
||||||
### 方法二:使用手动登录
|
|
||||||
|
### Method 2: Manual Login
|
||||||
#### 步骤 1:配置 `start.bat`
|
|
||||||
|
#### Step 1: Configure `start.bat`
|
||||||
1. **打开 `start.bat` 文件,将 `USE_MANUAL_LOGIN` 设置为 `true`:**
|
|
||||||
|
1. **Open `start.bat` file, set `USE_MANUAL_LOGIN` to `true`:**
|
||||||
```batch
|
|
||||||
set USE_MANUAL_LOGIN=true
|
```batch
|
||||||
```
|
set USE_MANUAL_LOGIN=true
|
||||||
|
```
|
||||||
2. **保存并关闭 `start.bat` 文件。**
|
|
||||||
|
2. **Save and close `start.bat` file.**
|
||||||
#### 步骤 2:启动服务并手动登录
|
|
||||||
|
#### Step 2: Start Service and Manual Login
|
||||||
1. **重命名 `config.example.mjs` 文件,将文件另存为 `config.mjs`。**
|
|
||||||
|
1. **Double-click to run `start.bat`.**
|
||||||
2. **双击运行 `start.bat`。**
|
|
||||||
|
2. **The program will start and automatically open a browser window.**
|
||||||
3. **程序将启动并自动打开浏览器窗口。**
|
|
||||||
|
3. **Manually log in to your You.com account in the pop-up browser window.**
|
||||||
4. **在弹出的浏览器窗口中手动登录的 You.com 账户。**
|
|
||||||
|
4. **After successful login, the program will automatically obtain the session information.**
|
||||||
5. **登录成功后,程序将自动获取的会话信息。**
|
|
||||||
|
#### Step 3: Configure Client
|
||||||
#### 步骤 3:配置客户端
|
|
||||||
|
*Same as Step 5 in Method 1*
|
||||||
*同方法一的步骤5*
|
|
||||||
|
---
|
||||||
---
|
|
||||||
|
## Optional Configurations
|
||||||
## 可选配置 / Optional Configurations
|
|
||||||
|
### Set Proxy
|
||||||
### 设置代理 / Set Proxy
|
|
||||||
|
If you need to set a proxy, please set the `http_proxy` and `https_proxy` environment variables in `start.bat`. For example:
|
||||||
如果需要设置代理,请在 `start.bat` 中设置 `http_proxy` 和 `https_proxy` 环境变量。例如:
|
|
||||||
|
```batch
|
||||||
```batch
|
set http_proxy=http://127.0.0.1:7890
|
||||||
set http_proxy=http://127.0.0.1:7890
|
set https_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.
|
||||||
|
*(If the browser crashes on startup, remove the proxy.)*
|
||||||
This project uses the local Chrome browser, which will automatically read and use the system proxy settings.
|
|
||||||
|
### Set AI Model
|
||||||
### 设置 AI 模型 / Set AI Model
|
|
||||||
|
You can switch the model used by setting the `AI_MODEL` environment variable. Supported models include (please refer to the official website for the latest models):
|
||||||
可以通过设置 `AI_MODEL` 环境变量来切换使用的模型。支持的模型包括(请参考官网获取最新模型):
|
|
||||||
|
- `gpt_4o`
|
||||||
- `gpt_4o`
|
- `gpt_4_turbo`
|
||||||
- `gpt_4_turbo`
|
- `gpt_4`
|
||||||
- `gpt_4`
|
- `claude_3_5_sonnet`
|
||||||
- `claude_3_5_sonnet`
|
- `claude_3_opus`
|
||||||
- `claude_3_opus`
|
- `claude_3_sonnet`
|
||||||
- `claude_3_sonnet`
|
- `claude_3_haiku`
|
||||||
- `claude_3_haiku`
|
- `claude_2`
|
||||||
- `claude_2`
|
- `llama3`
|
||||||
- `llama3`
|
- `gemini_pro`
|
||||||
- `gemini_pro`
|
- `gemini_1_5_pro`
|
||||||
- `gemini_1_5_pro`
|
- `databricks_dbrx_instruct`
|
||||||
- `databricks_dbrx_instruct`
|
- `command_r`
|
||||||
- `command_r`
|
- `command_r_plus`
|
||||||
- `command_r_plus`
|
- `zephyr`
|
||||||
- `zephyr`
|
|
||||||
|
For example:
|
||||||
例如:
|
|
||||||
|
```batch
|
||||||
```batch
|
set AI_MODEL=claude_3_opus
|
||||||
set AI_MODEL=claude_3_opus
|
```
|
||||||
```
|
|
||||||
|
### Enable Custom Chat Mode
|
||||||
### 启用自定义会话模式 / Enable Custom Chat Mode
|
|
||||||
|
When enabled, it can shorten system message length, disable internet connection, reduce waiting time, which may help break through limitations.
|
||||||
启用后,可以缩短系统消息长度、禁用联网、减少等待时间,可能有助于突破限制。
|
|
||||||
|
```batch
|
||||||
```batch
|
set USE_CUSTOM_MODE=true
|
||||||
set USE_CUSTOM_MODE=true
|
```
|
||||||
```
|
|
||||||
|
### Enable Mode Rotation
|
||||||
### 启用模式轮换 / Enable Mode Rotation
|
|
||||||
|
Mode rotation will only be enabled when both `USE_CUSTOM_MODE` and `ENABLE_MODE_ROTATION` are set to `true`.
|
||||||
只有当 `USE_CUSTOM_MODE` 和 `ENABLE_MODE_ROTATION` 都设置为 `true` 时,才会启用模式轮换功能。
|
|
||||||
|
```batch
|
||||||
```batch
|
set ENABLE_MODE_ROTATION=true
|
||||||
set ENABLE_MODE_ROTATION=true
|
```
|
||||||
```
|
|
||||||
|
### Enable Tunnel Access
|
||||||
### 启用隧道访问 / Enable Tunnel Access
|
|
||||||
|
If you need to access the local service from the external network, you can enable tunnel access. Both `ngrok` and `localtunnel` are supported.
|
||||||
如果需要从外网访问本地服务,可以启用隧道访问。支持 `ngrok` 和 `localtunnel`。
|
|
||||||
|
**Using ngrok:**
|
||||||
**使用 ngrok:**
|
|
||||||
|
1. **Set tunnel type:**
|
||||||
1. **设置隧道类型:**
|
|
||||||
|
```batch
|
||||||
```batch
|
set ENABLE_TUNNEL=true
|
||||||
set ENABLE_TUNNEL=true
|
set TUNNEL_TYPE=ngrok
|
||||||
set TUNNEL_TYPE=ngrok
|
```
|
||||||
```
|
|
||||||
|
2. **Set ngrok Auth Token (obtain from ngrok dashboard):**
|
||||||
2. **设置 ngrok Auth Token(从 ngrok 仪表板获取):**
|
|
||||||
|
```batch
|
||||||
```batch
|
set NGROK_AUTH_TOKEN=your_ngrok_auth_token
|
||||||
set NGROK_AUTH_TOKEN=your_ngrok_auth_token
|
```
|
||||||
```
|
|
||||||
|
3. **(Optional) Set custom domain (paid account):**
|
||||||
3. **(可选)设置自定义域名(付费账户):**
|
|
||||||
|
```batch
|
||||||
```batch
|
set NGROK_CUSTOM_DOMAIN=your_custom_domain
|
||||||
set NGROK_CUSTOM_DOMAIN=your_custom_domain
|
```
|
||||||
```
|
|
||||||
|
**Using localtunnel:**
|
||||||
**使用 localtunnel:**
|
|
||||||
|
1. **Set tunnel type:**
|
||||||
1. **设置隧道类型:**
|
|
||||||
|
```batch
|
||||||
```batch
|
set ENABLE_TUNNEL=true
|
||||||
set ENABLE_TUNNEL=true
|
set TUNNEL_TYPE=localtunnel
|
||||||
set TUNNEL_TYPE=localtunnel
|
```
|
||||||
```
|
|
||||||
|
2. **(Optional) Set subdomain:**
|
||||||
2. **(可选)设置子域名:**
|
|
||||||
|
```batch
|
||||||
```batch
|
set SUBDOMAIN=your_subdomain
|
||||||
set SUBDOMAIN=your_subdomain
|
```
|
||||||
```
|
|
||||||
|
---
|
||||||
---
|
|
||||||
|
## Important Notes
|
||||||
## 注意事项 / Important Notes
|
|
||||||
|
- **About Cloudflare CAPTCHA:**
|
||||||
- **关于 Cloudflare 人机验证:**
|
|
||||||
|
If a CAPTCHA prompt pops up during program operation, please complete the verification within 30 seconds.
|
||||||
如果在程序运行过程中弹出人机验证提示,请在30秒内完成验证。
|
|
||||||
|
- **About `ALLOW_NON_PRO` setting:**
|
||||||
- **关于 `ALLOW_NON_PRO` 设定:**
|
|
||||||
|
If set to `true`, it allows the use of non-subscription accounts, but functionality will be limited and may not work properly.
|
||||||
如果设置为 `true`,允许使用非订阅账户,但功能会受限,可能无法正常使用。
|
|
||||||
|
```batch
|
||||||
```batch
|
set ALLOW_NON_PRO=true
|
||||||
set ALLOW_NON_PRO=true
|
```
|
||||||
```
|
|
||||||
|
- **About `CUSTOM_END_MARKER` setting:**
|
||||||
- **关于 `CUSTOM_END_MARKER` 设定:**
|
|
||||||
|
When the output cannot stop, you can set a custom termination marker. The program will automatically stop output after detecting this marker.
|
||||||
当输出无法停止时,可设置自定义终止符,程序检测到该终止符后将自动停止输出。
|
|
||||||
|
```batch
|
||||||
```batch
|
set CUSTOM_END_MARKER="<YOUR_END_MARKER>"
|
||||||
set CUSTOM_END_MARKER="<YOUR_END_MARKER>"
|
```
|
||||||
```
|
|
||||||
|
- **About `ENABLE_DELAY_LOGIC` setting:**
|
||||||
- **关于 `ENABLE_DELAY_LOGIC` 设定:**
|
|
||||||
|
If requests are stuck, try setting this to `true`.
|
||||||
如果请求卡顿,尝试将其设置为 `true`。
|
|
||||||
|
```batch
|
||||||
```batch
|
set ENABLE_DELAY_LOGIC=true
|
||||||
set ENABLE_DELAY_LOGIC=true
|
```
|
||||||
```
|
|
||||||
|
- **About upload file format:**
|
||||||
- **关于上传文件格式:**
|
|
||||||
|
You can choose to upload files in `docx` or `txt` format.
|
||||||
可以选择上传文件的格式为 `docx` 或 `txt`。
|
|
||||||
|
```batch
|
||||||
```batch
|
set UPLOAD_FILE_FORMAT=docx
|
||||||
set UPLOAD_FILE_FORMAT=docx
|
```
|
||||||
```
|
|
||||||
- **关于403问题(基本只存在于旧版本)**
|
- **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.
|
||||||
|
|
||||||
新版本如果弹出人机验证提示,用户只需要在30秒内点击完成CloudFlare的人机验证,并且等待程序继续处理即可。
|
In the new version, if a CAPTCHA prompt pops up, users only need to complete the CloudFlare CAPTCHA within 30 seconds and wait for the program to continue processing.
|
||||||
|
|
||||||
cloudflare有一个风控分数。这个和你的TLS指纹、浏览器指纹、IP地址声誉等等有关系
|
Cloudflare has a risk control score. This is related to your TLS fingerprint, browser fingerprint, IP address reputation, etc.
|
||||||
我们这个项目一直用的TLS指纹和浏览器指纹就非常可疑(都是自动化库和Node内置TLS),分数直接拉满
|
The TLS fingerprint and browser fingerprint used in this project have always been very suspicious (all are automation libraries and Node built-in TLS), directly maxing out the score.
|
||||||
相当于已经预先有了30+30分数,剩下就看IP地址声誉(40分)你拿了几分
|
It's equivalent to having 30+30 points in advance, and the rest depends on how many points your IP address reputation takes (40 points)
|
||||||
(具体分数不详,只是举个例子)
|
(The specific scores are not detailed, just an example)
|
||||||
那如果你IP确实白,拿了0分,那你总共分数就是60。
|
So if your IP is indeed white and takes 0 points, your total score is 60.
|
||||||
假设you那边设置了分数高于80的要跳验证码,那现在就没事
|
Suppose You sets that scores higher than 80 require CAPTCHA, then there's no problem now.
|
||||||
如果你IP黑,拿了超过20分,那你就是>80分,你就要跳验证码,结果就是403
|
If your IP is black and takes more than 20 points, then you're >80 points, you need to do CAPTCHA, resulting in 403.
|
||||||
然后最近you觉得被薅狠了,或者别的啥原因,把这个分数设置成60以上的就要跳验证码
|
Then recently You felt it was being abused too much, or for some other reason, set it so that scores above 60 require CAPTCHA.
|
||||||
结果就我IP有点黑,不管怎么搞都过不去了。
|
As a result, my IP is a bit black, and I can't get through no matter what.
|
||||||
但是同样的IP,你用正常的Google Chrome访问,就没问题,因为它的指纹非常干净,所以前面的指纹分数就很低
|
But with the same IP, if you access with normal Google Chrome, there's no problem, because its fingerprint is very clean, so the previous fingerprint score is very low.
|
||||||
就算加上IP声誉分他也没到那条线
|
Even with the IP reputation score added, it doesn't reach that line.
|
||||||
总之以上是一个简化的版本,CF抗bot还有很多指标、很多策略
|
In short, the above is a simplified version, CF has many more indicators and strategies for anti-bot.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 在 Linux 上部署 / Deploy on Linux
|
## Deploy on Linux
|
||||||
|
|
||||||
可以使用 Docker 进行部署,请参照项目中的 `Dockerfile`。
|
You can deploy using Docker, please refer to the `Dockerfile` in the project.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 常见问题 / FAQ
|
## FAQ
|
||||||
|
|
||||||
**Q:** 如何解决 npm 安装依赖失败的问题?
|
**Q:** How to solve the problem of npm failing to install dependencies?
|
||||||
|
|
||||||
**A:** 请确保的网络连接稳定,必要时使用全局代理。
|
**A:** Please ensure your network connection is stable, use global proxy if necessary.
|
||||||
|
|
||||||
**Q:** 为什么程序提示 "两种模式均达到请求上限"?
|
**Q:** Why does the program prompt "Both modes have reached the request limit"?
|
||||||
|
|
||||||
**A:** 这可能是因为频繁请求导致模式被暂时禁用,建议稍等一段时间再尝试。
|
**A:** This may be because frequent requests have caused the mode to be temporarily disabled. It is recommended to wait for a while before trying again.
|
||||||
|
|
||||||
**Q:** 如何切换模型?
|
**Q:** How to switch models?
|
||||||
|
|
||||||
**A:** 编辑 `start.bat` 中的 `AI_MODEL` 环境变量,设置为想使用的模型名称(已经可以在SillyTavern设置了)。
|
**A:** Edit the `AI_MODEL` environment variable in `start.bat`, set it to the name of the model you want to use (can now be set in SillyTavern).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 免责声明 / Disclaimer
|
## 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.
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
1. **Install necessary software:**
|
|
||||||
|
|
||||||
- Node.js
|
|
||||||
- Git
|
|
||||||
- Python
|
|
||||||
- Visual C++ Build Tools ([Download link](https://visualstudio.microsoft.com/visual-cpp-build-tools/))
|
|
||||||
|
|
||||||
2. **Obtain a You.com account and subscribe to Pro or Team plan, then log in.**
|
|
||||||
|
|
||||||
3. **Global proxy is recommended to ensure stable network connection.**
|
|
||||||
|
|
||||||
4. **If needed, you can set up a proxy in the `start.bat` file.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Setup Steps
|
|
||||||
|
|
||||||
### Method 1: Login using Cookie (Default)
|
|
||||||
|
|
||||||
#### Step 1: Obtain Cookie
|
|
||||||
|
|
||||||
1. **Open browser and log in to [you.com](https://you.com).**
|
|
||||||
|
|
||||||
2. **Press `F12` to open developer tools, find the "Console" tab.**
|
|
||||||
|
|
||||||
3. **Enter the following code in the console and press enter, then copy all output content (Cookie):**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
console.log(document.cookie);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 2: Configure Project
|
|
||||||
|
|
||||||
1. **Download or clone this project code, unzip.**
|
|
||||||
|
|
||||||
2. **Edit `config.example.mjs` file, paste the Cookie obtained in the previous step.**
|
|
||||||
|
|
||||||
If there are multiple Cookies, add them in the following format, then save the file as `config.mjs`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export const config = {
|
|
||||||
"sessions": [
|
|
||||||
{
|
|
||||||
"cookie": `cookie1`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": `cookie2`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": `cookie3`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 3: Configure Environment Variables
|
|
||||||
|
|
||||||
1. **Open `start.bat` file, set environment variables as needed.**
|
|
||||||
|
|
||||||
#### Step 4: Start Service
|
|
||||||
|
|
||||||
1. **Double-click to run `start.bat`.**
|
|
||||||
|
|
||||||
2. **Wait for the program to install dependencies and start the service.**
|
|
||||||
|
|
||||||
#### Step 5: Configure Client
|
|
||||||
|
|
||||||
1. **In SillyTavern, select **Custom (OpenAI-compatible)**.**
|
|
||||||
|
|
||||||
2. **Set the reverse proxy address to `http://127.0.0.1:8080/v1`.**
|
|
||||||
|
|
||||||
3. **Reverse proxy password needs to be filled (any value will do, unless `PASSWORD` is set in `start.bat`).**
|
|
||||||
|
|
||||||
4. **Start using. If it fails or there's no result, try multiple retries.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Method 2: Manual Login
|
|
||||||
|
|
||||||
#### Step 1: Configure `start.bat`
|
|
||||||
|
|
||||||
1. **Open `start.bat` file, set `USE_MANUAL_LOGIN` to `true`:**
|
|
||||||
|
|
||||||
```batch
|
|
||||||
set USE_MANUAL_LOGIN=true
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Save and close `start.bat` file.**
|
|
||||||
|
|
||||||
#### Step 2: Start Service and Manual Login
|
|
||||||
|
|
||||||
1. **Double-click to run `start.bat`.**
|
|
||||||
|
|
||||||
2. **The program will start and automatically open a browser window.**
|
|
||||||
|
|
||||||
3. **Manually log in to your You.com account in the pop-up browser window.**
|
|
||||||
|
|
||||||
4. **After successful login, the program will automatically obtain the session information.**
|
|
||||||
|
|
||||||
#### Step 3: Configure Client
|
|
||||||
|
|
||||||
*Same as Step 5 in Method 1*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Optional Configurations
|
|
||||||
|
|
||||||
### Set Proxy
|
|
||||||
|
|
||||||
If you need to set a proxy, please set the `http_proxy` and `https_proxy` environment variables in `start.bat`. For example:
|
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
||||||
### Set AI Model
|
|
||||||
|
|
||||||
You can switch the model used by setting the `AI_MODEL` environment variable. Supported models include (please refer to the official website for the latest models):
|
|
||||||
|
|
||||||
- `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`
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```batch
|
|
||||||
set AI_MODEL=claude_3_opus
|
|
||||||
```
|
|
||||||
|
|
||||||
### Enable Custom Chat Mode
|
|
||||||
|
|
||||||
When enabled, it can shorten system message length, disable internet connection, reduce waiting time, which may help break through limitations.
|
|
||||||
|
|
||||||
```batch
|
|
||||||
set USE_CUSTOM_MODE=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Enable Mode Rotation
|
|
||||||
|
|
||||||
Mode rotation will only be enabled when both `USE_CUSTOM_MODE` and `ENABLE_MODE_ROTATION` are set to `true`.
|
|
||||||
|
|
||||||
```batch
|
|
||||||
set ENABLE_MODE_ROTATION=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Enable Tunnel Access
|
|
||||||
|
|
||||||
If you need to access the local service from the external network, you can enable tunnel access. Both `ngrok` and `localtunnel` are supported.
|
|
||||||
|
|
||||||
**Using ngrok:**
|
|
||||||
|
|
||||||
1. **Set tunnel type:**
|
|
||||||
|
|
||||||
```batch
|
|
||||||
set ENABLE_TUNNEL=true
|
|
||||||
set TUNNEL_TYPE=ngrok
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Set ngrok Auth Token (obtain from ngrok dashboard):**
|
|
||||||
|
|
||||||
```batch
|
|
||||||
set NGROK_AUTH_TOKEN=your_ngrok_auth_token
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **(Optional) Set custom domain (paid account):**
|
|
||||||
|
|
||||||
```batch
|
|
||||||
set NGROK_CUSTOM_DOMAIN=your_custom_domain
|
|
||||||
```
|
|
||||||
|
|
||||||
**Using localtunnel:**
|
|
||||||
|
|
||||||
1. **Set tunnel type:**
|
|
||||||
|
|
||||||
```batch
|
|
||||||
set ENABLE_TUNNEL=true
|
|
||||||
set TUNNEL_TYPE=localtunnel
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **(Optional) Set subdomain:**
|
|
||||||
|
|
||||||
```batch
|
|
||||||
set SUBDOMAIN=your_subdomain
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
- **About Cloudflare CAPTCHA:**
|
|
||||||
|
|
||||||
If a CAPTCHA prompt pops up during program operation, please complete the verification within 30 seconds.
|
|
||||||
|
|
||||||
- **About `ALLOW_NON_PRO` setting:**
|
|
||||||
|
|
||||||
If set to `true`, it allows the use of non-subscription accounts, but functionality will be limited and may not work properly.
|
|
||||||
|
|
||||||
```batch
|
|
||||||
set ALLOW_NON_PRO=true
|
|
||||||
```
|
|
||||||
|
|
||||||
- **About `CUSTOM_END_MARKER` setting:**
|
|
||||||
|
|
||||||
When the output cannot stop, you can set a custom termination marker. The program will automatically stop output after detecting this marker.
|
|
||||||
|
|
||||||
```batch
|
|
||||||
set CUSTOM_END_MARKER="<YOUR_END_MARKER>"
|
|
||||||
```
|
|
||||||
|
|
||||||
- **About `ENABLE_DELAY_LOGIC` setting:**
|
|
||||||
|
|
||||||
If requests are stuck, try setting this to `true`.
|
|
||||||
|
|
||||||
```batch
|
|
||||||
set ENABLE_DELAY_LOGIC=true
|
|
||||||
```
|
|
||||||
|
|
||||||
- **About upload file format:**
|
|
||||||
|
|
||||||
You can choose to upload files in `docx` or `txt` format.
|
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
||||||
In the new version, if a CAPTCHA prompt pops up, users only need to complete the CloudFlare CAPTCHA within 30 seconds and wait for the program to continue processing.
|
|
||||||
|
|
||||||
Cloudflare has a risk control score. This is related to your TLS fingerprint, browser fingerprint, IP address reputation, etc.
|
|
||||||
The TLS fingerprint and browser fingerprint used in this project have always been very suspicious (all are automation libraries and Node built-in TLS), directly maxing out the score.
|
|
||||||
It's equivalent to having 30+30 points in advance, and the rest depends on how many points your IP address reputation takes (40 points)
|
|
||||||
(The specific scores are not detailed, just an example)
|
|
||||||
So if your IP is indeed white and takes 0 points, your total score is 60.
|
|
||||||
Suppose You sets that scores higher than 80 require CAPTCHA, then there's no problem now.
|
|
||||||
If your IP is black and takes more than 20 points, then you're >80 points, you need to do CAPTCHA, resulting in 403.
|
|
||||||
Then recently You felt it was being abused too much, or for some other reason, set it so that scores above 60 require CAPTCHA.
|
|
||||||
As a result, my IP is a bit black, and I can't get through no matter what.
|
|
||||||
But with the same IP, if you access with normal Google Chrome, there's no problem, because its fingerprint is very clean, so the previous fingerprint score is very low.
|
|
||||||
Even with the IP reputation score added, it doesn't reach that line.
|
|
||||||
In short, the above is a simplified version, CF has many more indicators and strategies for anti-bot.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deploy on Linux
|
|
||||||
|
|
||||||
You can deploy using Docker, please refer to the `Dockerfile` in the project.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## FAQ
|
|
||||||
|
|
||||||
**Q:** How to solve the problem of npm failing to install dependencies?
|
|
||||||
|
|
||||||
**A:** Please ensure your network connection is stable, use global proxy if necessary.
|
|
||||||
|
|
||||||
**Q:** Why does the program prompt "Both modes have reached the request limit"?
|
|
||||||
|
|
||||||
**A:** This may be because frequent requests have caused the mode to be temporarily disabled. It is recommended to wait for a while before trying again.
|
|
||||||
|
|
||||||
**Q:** How to switch models?
|
|
||||||
|
|
||||||
**A:** Edit the `AI_MODEL` environment variable in `start.bat`, set it to the name of the model you want to use (can now be set in 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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user