fix: 修复无需鉴权的 Socks5 代理无法使用的问题

This commit is contained in:
foxhui
2026-01-25 03:54:29 +08:00
Unverified
parent 5b7d1e9322
commit 02847223dc
11 changed files with 650 additions and 180 deletions
+6
View File
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.5.1] - 2026-01-24
### 🐛 Fixed
- **代理**
- 修复无需鉴权的 Socks5 代理无法使用的问题
## [3.5.0] - 2026-01-23
### ✨ Added
+2
View File
@@ -25,9 +25,11 @@
"fingerprint-generator": "^2.1.78",
"ghost-cursor-playwright-port": "^1.4.3",
"got-scraping": "^4.1.2",
"https-proxy-agent": "^7.0.6",
"playwright-core": "^1.57.0",
"proxy-chain": "^2.6.0",
"sharp": "^0.34.5",
"socks-proxy-agent": "^8.0.5",
"yaml": "^2.8.2"
}
}
+518
View File
@@ -0,0 +1,518 @@
/**
* ========================================
* PATCHED VERSION - WebAI-2API
* ========================================
*
* 修改内容:
* - 第 490 行: 修复 SOCKS5 代理 URL.origin 返回 "null" 的问题
* 原代码: server: proxyUrl.origin,
* 修改为: server: proxyUrl.protocol + '//' + proxyUrl.host,
*
* 问题原因:
* JavaScript 的 new URL('socks5://...').origin 对非标准协议返回 "null" 字符串,
* 导致浏览器报错 NS_ERROR_UNKNOWN_PROXY_HOST
*
* 搜索 "PATCHED" 可定位所有修改位置
* ========================================
*/
// from browserforge.fingerprints import Fingerprint, Screen
// from screeninfo import get_monitors
// from ua_parser import user_agent_parser
import { readFileSync } from "node:fs";
import path from "node:path";
import { UAParser } from "ua-parser-js";
import { addDefaultAddons, confirmPaths, } from "./addons.js";
import { InvalidOS, InvalidPropertyType, NonFirefoxFingerprint, UnknownProperty, } from "./exceptions.js";
import { fromBrowserforge, generateFingerprint, SUPPORTED_OS, } from "./fingerprints.js";
import { publicIP, validIPv4, validIPv6 } from "./ip.js";
import { geoipAllowed, getGeolocation, handleLocales } from "./locale.js";
import FONTS from "./mappings/fonts.config.js";
import { getPath, installedVerStr, launchPath, OS_NAME } from "./pkgman.js";
import { LeakWarning } from "./warnings.js";
import { sampleWebGL } from "./webgl/sample.js";
// Camoufox preferences to cache previous pages and requests
const CACHE_PREFS = {
"browser.sessionhistory.max_entries": 10,
"browser.sessionhistory.max_total_viewers": -1,
"browser.cache.memory.enable": true,
"browser.cache.disk_cache_ssl": true,
"browser.cache.disk.smart_size.enabled": true,
};
function getEnvVars(configMap, userAgentOS) {
const envVars = {};
let updatedConfigData;
try {
updatedConfigData = new TextEncoder().encode(JSON.stringify(configMap));
}
catch (e) {
console.error(`Error updating config: ${e}`);
process.exit(1);
}
const chunkSize = OS_NAME === "win" ? 2047 : 32767;
const configStr = new TextDecoder().decode(updatedConfigData);
for (let i = 0; i < configStr.length; i += chunkSize) {
const chunk = configStr.slice(i, i + chunkSize);
const envName = `CAMOU_CONFIG_${Math.floor(i / chunkSize) + 1}`;
try {
envVars[envName] = chunk;
}
catch (e) {
console.error(`Error setting ${envName}: ${e}`);
process.exit(1);
}
}
if (OS_NAME === "lin") {
const fontconfigPath = getPath(path.join("fontconfig", userAgentOS));
envVars.FONTCONFIG_PATH = fontconfigPath;
}
return envVars;
}
export function getAsBooleanFromENV(name, defaultValue) {
const value = process.env[name];
if (value === "false" || value === "0")
return false;
if (value)
return true;
return !!defaultValue;
}
function loadProperties(filePath) {
let propFile;
filePath = filePath?.toString();
if (filePath) {
propFile = path.join(path.dirname(filePath), "properties.json");
}
else {
propFile = getPath("properties.json");
}
const propData = readFileSync(propFile).toString();
const propDict = JSON.parse(propData);
return propDict.reduce((acc, prop) => {
acc[prop.property] = prop.type;
return acc;
}, {});
}
function validateConfig(configMap, path) {
const propertyTypes = loadProperties(path);
for (const [key, value] of Object.entries(configMap)) {
const expectedType = propertyTypes[key];
if (!expectedType) {
throw new UnknownProperty(`Unknown property ${key} in config`);
}
if (!validateType(value, expectedType)) {
throw new InvalidPropertyType(`Invalid type for property ${key}. Expected ${expectedType}, got ${typeof value}`);
}
}
}
function validateType(value, expectedType) {
switch (expectedType) {
case "str":
return typeof value === "string";
case "int":
return Number.isInteger(value);
case "uint":
return Number.isInteger(value) && value >= 0;
case "double":
return typeof value === "number";
case "bool":
return typeof value === "boolean";
case "array":
return Array.isArray(value);
case "dict":
return (typeof value === "object" && value !== null && !Array.isArray(value));
default:
return false;
}
}
function getTargetOS(config) {
if (config["navigator.userAgent"]) {
return determineUAOS(config["navigator.userAgent"]);
}
return OS_NAME;
}
function determineUAOS(userAgent) {
const parser = new UAParser(userAgent);
const parsedUA = parser.getOS().name;
if (!parsedUA) {
throw new Error("Could not determine OS from user agent");
}
if (parsedUA.startsWith("macOS")) {
return "mac";
}
if (parsedUA.startsWith("Windows")) {
return "win";
}
return "lin";
}
function getScreenCons(headless) {
if (headless === false) {
return undefined;
}
// TODO - Implement getMonitors
// try {
// const monitors = getMonitors();
// if (!monitors.length) {
// return undefined;
// }
// const monitor = monitors.reduce((prev, curr) => (prev.width * prev.height > curr.width * curr.height ? prev : curr));
// return { maxWidth: monitor.width, maxHeight: monitor.height };
// } catch {
// return undefined;
// }
return undefined;
}
function updateFonts(config, targetOS) {
const fonts = FONTS[targetOS];
if (config.fonts) {
config.fonts = Array.from(new Set([...fonts, ...config.fonts]));
}
else {
config.fonts = fonts;
}
}
function checkCustomFingerprint(fingerprint) {
const parser = new UAParser(fingerprint.navigator.userAgent);
const browserName = parser.getBrowser().name || "Non-Firefox";
if (browserName !== "Firefox") {
throw new NonFirefoxFingerprint(`"${browserName}" fingerprints are not supported in Camoufox. Using fingerprints from a browser other than Firefox WILL lead to detection. If this is intentional, pass i_know_what_im_doing=True.`);
}
LeakWarning.warn("custom_fingerprint", false);
}
function validateOS(os) {
if (!os)
return undefined;
if (Array.isArray(os)) {
os.every(validateOS);
return [...os];
}
if (!SUPPORTED_OS.includes(os)) {
throw new InvalidOS(`Camoufox does not support the OS: '${os}'`);
}
return [os];
}
function _cleanLocals(data) {
delete data.playwright;
delete data.persistentContext;
return data;
}
function mergeInto(target, source) {
Object.entries(source).forEach(([key, value]) => {
if (!(key in target)) {
target[key] = value;
}
});
}
function setInto(target, key, value) {
if (!(key in target)) {
target[key] = value;
}
}
function isDomainSet(config, ...properties) {
return properties.some((prop) => {
if (prop.endsWith(".") || prop.endsWith(":")) {
return Object.keys(config).some((key) => key.startsWith(prop));
}
return prop in config;
});
}
function warnManualConfig(config) {
if (isDomainSet(config, "navigator.language", "navigator.languages", "headers.Accept-Language", "locale:")) {
LeakWarning.warn("locale", false);
}
if (isDomainSet(config, "geolocation:", "timezone")) {
LeakWarning.warn("geolocation", false);
}
if (isDomainSet(config, "headers.User-Agent")) {
LeakWarning.warn("header-ua", false);
}
if (isDomainSet(config, "navigator.")) {
LeakWarning.warn("navigator", false);
}
if (isDomainSet(config, "screen.", "window.", "document.body.")) {
LeakWarning.warn("viewport", false);
}
}
async function _asyncAttachVD(browser, virtualDisplay) {
if (!virtualDisplay) {
return browser;
}
const originalClose = browser.close;
browser.close = async (...args) => {
await originalClose.apply(browser, ...args);
if (virtualDisplay) {
virtualDisplay.kill();
}
};
browser._virtualDisplay = virtualDisplay;
return browser;
}
export function syncAttachVD(browser, virtualDisplay) {
/**
* Attaches the virtual display to the sync browser cleanup
*/
if (!virtualDisplay) {
// Skip if no virtual display is provided
return browser;
}
const originalClose = browser.close;
browser.close = (...args) => {
originalClose.apply(browser, ...args);
if (virtualDisplay) {
virtualDisplay.kill();
}
};
browser._virtualDisplay = virtualDisplay;
return browser;
}
/**
* Convert a Playwright proxy string to a URL object.
*
* Implementation from https://github.com/microsoft/playwright/blob/3873b72ac1441ca691f7594f0ed705bd84518f93/packages/playwright-core/src/server/browserContext.ts#L737-L747
*/
function getProxyUrl(proxy) {
if (!proxy)
return null;
if (typeof proxy === "string") {
return new URL(proxy);
}
const { server, username, password } = proxy;
let url;
try {
// new URL('127.0.0.1:8080') throws
// new URL('localhost:8080') fails to parse host or protocol
// In both of these cases, we need to try re-parse URL with `http://` prefix.
url = new URL(server);
if (!url.host || !url.protocol)
url = new URL(`http://${server}`);
}
catch (_e) {
url = new URL(`http://${server}`);
}
if (username)
url.username = username;
if (password)
url.password = password;
return url;
}
export async function launchOptions({ config, os, block_images, block_webrtc, block_webgl, disable_coop, webgl_config, geoip, humanize, locale, addons, fonts, custom_fonts_only, exclude_addons, screen, window, fingerprint, ff_version, headless, main_world_eval, executable_path, firefox_user_prefs, proxy, enable_cache, args, env, i_know_what_im_doing, debug, virtual_display, ...launch_options }) {
// Build the config
if (!config) {
config = {};
}
// Set default values for optional arguments
if (headless === undefined) {
headless = false;
}
if (!addons) {
addons = [];
}
if (!args) {
args = [];
}
if (!firefox_user_prefs) {
firefox_user_prefs = {};
}
if (custom_fonts_only === undefined) {
custom_fonts_only = false;
}
if (i_know_what_im_doing === undefined) {
i_know_what_im_doing = false;
}
if (!env) {
env = process.env;
}
if (typeof executable_path === "string") {
// Convert executable path to a Path object
executable_path = path.resolve(executable_path);
}
// Handle virtual display
if (virtual_display) {
env.DISPLAY = virtual_display;
}
// Warn the user for manual config settings
if (!i_know_what_im_doing) {
warnManualConfig(config);
}
const operatingSystems = validateOS(os);
// webgl_config requires OS to be set
if (!operatingSystems && webgl_config) {
throw new Error("OS must be set when using webgl_config");
}
// Add the default addons
addDefaultAddons(addons, exclude_addons);
// Confirm all addon paths are valid
if (addons.length > 0) {
confirmPaths(addons);
config.addons = addons;
}
// Get the Firefox version
let ff_version_str;
if (ff_version) {
ff_version_str = ff_version.toString();
LeakWarning.warn("ff_version", i_know_what_im_doing);
}
else {
ff_version_str = installedVerStr().split(".", 1)[0];
}
// Generate a fingerprint
if (!fingerprint) {
fingerprint = generateFingerprint(window, {
screen: screen || getScreenCons(headless || "DISPLAY" in env),
operatingSystems,
});
}
else {
// Or use the one passed by the user
if (!i_know_what_im_doing) {
checkCustomFingerprint(fingerprint);
}
}
// Inject the fingerprint into the config
mergeInto(config, fromBrowserforge(fingerprint, ff_version_str));
const targetOS = getTargetOS(config);
// Set a random window.history.length
setInto(config, "window.history.length", Math.floor(Math.random() * 5) + 1);
// Update fonts list
if (fonts) {
config.fonts = fonts;
}
if (custom_fonts_only) {
firefox_user_prefs["gfx.bundled-fonts.activate"] = 0;
if (fonts) {
// The user has passed their own fonts, and OS fonts are disabled.
LeakWarning.warn("custom_fonts_only");
}
else {
// OS fonts are disabled, and the user has not passed their own fonts either.
throw new Error("No custom fonts were passed, but `custom_fonts_only` is enabled.");
}
}
else {
updateFonts(config, targetOS);
}
// Set a fixed font spacing seed
setInto(config, "fonts:spacing_seed", Math.floor(Math.random() * 1_073_741_824));
// Handle proxy
const proxyUrl = getProxyUrl(proxy);
// Set geolocation
if (geoip) {
geoipAllowed();
// Find the user's IP address
geoip = await publicIP(proxyUrl?.href);
// Spoof WebRTC if not blocked
if (!block_webrtc) {
if (validIPv4(geoip)) {
setInto(config, "webrtc:ipv4", geoip);
firefox_user_prefs["network.dns.disableIPv6"] = true;
}
else if (validIPv6(geoip)) {
setInto(config, "webrtc:ipv6", geoip);
}
}
const geolocation = await getGeolocation(geoip);
config = { ...config, ...geolocation.asConfig() };
}
// Raise a warning when a proxy is being used without spoofing geolocation.
// This is a very bad idea; the warning cannot be ignored with i_know_what_im_doing.
if (proxyUrl &&
!proxyUrl.hostname.includes("localhost") &&
!isDomainSet(config, "geolocation:")) {
LeakWarning.warn("proxy_without_geoip");
}
// Set locale
if (locale) {
handleLocales(locale, config);
}
// Pass the humanize option
if (humanize) {
setInto(config, "humanize", true);
if (typeof humanize === "number") {
setInto(config, "humanize:maxTime", humanize);
}
}
// Enable the main world context creation
if (main_world_eval) {
setInto(config, "allowMainWorld", true);
}
// Set Firefox user preferences
if (block_images) {
LeakWarning.warn("block_images", i_know_what_im_doing);
firefox_user_prefs["permissions.default.image"] = 2;
}
if (block_webrtc) {
firefox_user_prefs["media.peerconnection.enabled"] = false;
}
if (disable_coop) {
LeakWarning.warn("disable_coop", i_know_what_im_doing);
firefox_user_prefs["browser.tabs.remote.useCrossOriginOpenerPolicy"] =
false;
}
// Allow allow_webgl parameter for backwards compatibility
if (block_webgl || launch_options.allow_webgl === false) {
firefox_user_prefs["webgl.disabled"] = true;
LeakWarning.warn("block_webgl", i_know_what_im_doing);
}
else {
// If the user has provided a specific WebGL vendor/renderer pair, use it
let webgl_fp;
if (webgl_config) {
webgl_fp = await sampleWebGL(targetOS, ...webgl_config);
}
else {
webgl_fp = await sampleWebGL(targetOS);
}
const { webGl2Enabled, ...webGlConfig } = webgl_fp;
// Merge the WebGL fingerprint into the config
mergeInto(config, webGlConfig);
// Set the WebGL preferences
mergeInto(firefox_user_prefs, {
"webgl.enable-webgl2": webGl2Enabled,
"webgl.force-enabled": true,
});
}
// Canvas anti-fingerprinting
mergeInto(config, {
"canvas:aaOffset": Math.floor(Math.random() * 101) - 50, // nosec
"canvas:aaCapOffset": true,
});
// Cache previous pages, requests, etc (uses more memory)
if (enable_cache) {
mergeInto(firefox_user_prefs, CACHE_PREFS);
}
// Print the config if debug is enabled
if (debug) {
console.debug("[DEBUG] Config:");
console.debug(config);
}
// Validate the config
validateConfig(config, executable_path);
//Prepare environment variables to pass to Camoufox
const env_vars = {
...getEnvVars(config, targetOS),
...process.env,
};
// Prepare the executable path
if (executable_path) {
executable_path = executable_path.toString();
}
else {
executable_path = launchPath();
}
const out = {
executablePath: executable_path,
args: args,
env: env_vars,
firefoxUserPrefs: firefox_user_prefs,
proxy: proxyUrl
? {
server: proxyUrl.protocol + '//' + proxyUrl.host, // PATCHED: fix socks5 origin=null
username: proxyUrl.username,
password: proxyUrl.password,
bypass: typeof proxy === "string" ? undefined : proxy?.bypass,
}
: undefined,
headless: headless,
...launch_options,
};
return out;
}
+17
View File
@@ -29,6 +29,9 @@ importers:
got-scraping:
specifier: ^4.1.2
version: 4.1.2
https-proxy-agent:
specifier: ^7.0.6
version: 7.0.6
playwright-core:
specifier: ^1.57.0
version: 1.57.0
@@ -38,6 +41,9 @@ importers:
sharp:
specifier: ^0.34.5
version: 0.34.5
socks-proxy-agent:
specifier: ^8.0.5
version: 8.0.5
yaml:
specifier: ^2.8.2
version: 2.8.2
@@ -689,6 +695,10 @@ packages:
resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==}
engines: {node: '>=10.19.0'}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
iconv-lite@0.5.2:
resolution: {integrity: sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==}
engines: {node: '>=0.10.0'}
@@ -1705,6 +1715,13 @@ snapshots:
quick-lru: 5.1.1
resolve-alpn: 1.2.1
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
transitivePeerDependencies:
- supports-color
iconv-lite@0.5.2:
dependencies:
safer-buffer: 2.1.2
+58 -124
View File
@@ -19,7 +19,8 @@ import { fileURLToPath } from 'url';
import compressing from 'compressing';
import { logger } from '../src/utils/logger.js';
import { select, input } from '@inquirer/prompts';
import { anonymizeProxy, closeAnonymizedProxy } from 'proxy-chain';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = path.join(__dirname, '..');
@@ -150,67 +151,45 @@ function validateABI(abi) {
* @param {number} maxRetries - 最大重试次数
*/
async function downloadFile(url, destPath, proxyUrl = null, maxRetries = 3) {
// 如果是 SOCKS5 代理,先转换为本地 HTTP 代理
let effectiveProxyUrl = proxyUrl;
let anonymizedProxy = null;
if (proxyUrl && proxyUrl.startsWith('socks5://')) {
try {
logger.info('初始化', `检测到 SOCKS5 代理,正在转换为 HTTP 代理...`);
anonymizedProxy = await anonymizeProxy(proxyUrl);
effectiveProxyUrl = anonymizedProxy;
logger.info('初始化', `SOCKS5 代理已转换: ${anonymizedProxy}`);
} catch (error) {
logger.error('初始化', `SOCKS5 代理转换失败: ${error.message}`);
throw error;
}
if (proxyUrl) {
const proxyType = proxyUrl.startsWith('socks') ? 'SOCKS5' : 'HTTP';
logger.info('初始化', `使用 ${proxyType} 代理: ${proxyUrl}`);
}
try {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
if (attempt > 1) {
logger.info('初始化', `${attempt}/${maxRetries} 次尝试下载...`);
// 删除之前失败的文件
try {
if (fs.existsSync(destPath)) {
fs.unlinkSync(destPath);
}
} catch (e) { }
} else {
logger.info('初始化', `开始下载: ${url}`);
}
await downloadFileOnce(url, destPath, effectiveProxyUrl);
return destPath;
} catch (error) {
logger.error('初始化', `下载失败 (尝试 ${attempt}/${maxRetries}): ${error.message}`);
if (attempt === maxRetries) {
throw error;
}
// 等待后重试(递增延迟)
const delay = attempt * 2000;
logger.info('初始化', `${delay / 1000} 秒后重试...`);
await new Promise(resolve => setTimeout(resolve, delay));
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
if (attempt > 1) {
logger.info('初始化', `${attempt}/${maxRetries} 次尝试下载...`);
// 删除之前失败的文件
try {
if (fs.existsSync(destPath)) {
fs.unlinkSync(destPath);
}
} catch (e) { }
} else {
logger.info('初始化', `开始下载: ${url}`);
}
}
} finally {
// 清理 SOCKS5 代理资源
if (anonymizedProxy) {
try {
await closeAnonymizedProxy(anonymizedProxy, true);
logger.debug('初始化', '已关闭临时代理桥接');
} catch (e) { }
await downloadFileOnce(url, destPath, proxyUrl);
return destPath;
} catch (error) {
logger.error('初始化', `下载失败 (尝试 ${attempt}/${maxRetries}): ${error.message}`);
if (attempt === maxRetries) {
throw error;
}
// 等待后重试(递增延迟)
const delay = attempt * 2000;
logger.info('初始化', `${delay / 1000} 秒后重试...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
/**
* 单次下载尝试(内部函数)
* 使用 Node.js 原生 http/https 模块,手动管理超时
* 只有在指定时间内没有任何数据传输才会触发超时
* 使用 Node.js 原生 http/https 模块,支持 SOCKS5 和 HTTP 代理
*/
async function downloadFileOnce(url, destPath, proxyUrl = null) {
const IDLE_TIMEOUT = 180000; // 3 分钟无数据传输才超时
@@ -232,25 +211,25 @@ async function downloadFileOnce(url, destPath, proxyUrl = null) {
}
};
// 如果有 HTTP 代理(注意:这里只支持 HTTP 代理,SOCKS 需要额外处理)
// 创建代理 agent
let httpModule = isHttps ? https : http;
if (proxyUrl && proxyUrl.startsWith('http')) {
const proxyUrlObj = new URL(proxyUrl);
// 使用 CONNECT 隧道代理
requestOptions = {
hostname: proxyUrlObj.hostname,
port: proxyUrlObj.port || 80,
method: 'CONNECT',
path: `${urlObj.hostname}:${urlObj.port || (isHttps ? 443 : 80)}`,
headers: {
'Host': `${urlObj.hostname}:${urlObj.port || (isHttps ? 443 : 80)}`
}
};
if (proxyUrlObj.username) {
const auth = Buffer.from(`${proxyUrlObj.username}:${proxyUrlObj.password || ''}`).toString('base64');
requestOptions.headers['Proxy-Authorization'] = `Basic ${auth}`;
let agent = null;
if (proxyUrl) {
if (proxyUrl.startsWith('socks')) {
// SOCKS5 代理,使用 socks-proxy-agent
logger.debug('初始化', `使用 SOCKS5 代理: ${proxyUrl}`);
agent = new SocksProxyAgent(proxyUrl);
} else if (proxyUrl.startsWith('http')) {
// HTTP 代理,使用 https-proxy-agent
logger.debug('初始化', `使用 HTTP 代理: ${proxyUrl}`);
agent = new HttpsProxyAgent(proxyUrl);
}
httpModule = http; // 代理连接始终是 HTTP
}
// 添加 agent 到请求选项
if (agent) {
requestOptions.agent = agent;
}
const fileStream = fs.createWriteStream(destPath);
@@ -362,60 +341,15 @@ async function downloadFileOnce(url, destPath, proxyUrl = null) {
resetIdleTimer();
// 如果使用代理,先建立隧道
if (proxyUrl && proxyUrl.startsWith('http')) {
req = http.request(requestOptions);
req.on('connect', (res, socket) => {
if (res.statusCode !== 200) {
cleanup();
reject(new Error(`代理连接失败: ${res.statusCode}`));
return;
}
// 通过隧道发起真正的 HTTPS 请求
const tunnelOptions = {
hostname: urlObj.hostname,
port: urlObj.port || (isHttps ? 443 : 80),
path: urlObj.pathname + urlObj.search,
method: 'GET',
headers: {
'User-Agent': 'Wget/1.21.4 (linux-gnu)',
'Accept': '*/*',
'Accept-Encoding': 'identity',
'Connection': 'keep-alive',
'Host': urlObj.host
},
socket: socket,
agent: false
};
const tunnelReq = (isHttps ? https : http).request(tunnelOptions, handleResponse);
tunnelReq.on('error', (error) => {
if (finished) return;
cleanup();
try { fs.unlinkSync(destPath); } catch (e) { }
reject(error);
});
tunnelReq.end();
});
req.on('error', (error) => {
if (finished) return;
cleanup();
try { fs.unlinkSync(destPath); } catch (e) { }
reject(error);
});
req.end();
} else {
// 直连模式
req = httpModule.request(requestOptions, handleResponse);
req.on('error', (error) => {
if (finished) return;
cleanup();
try { fs.unlinkSync(destPath); } catch (e) { }
reject(error);
});
req.end();
}
// 统一使用 httpModule.request 发起请求(agent 会自动处理代理)
req = httpModule.request(requestOptions, handleResponse);
req.on('error', (error) => {
if (finished) return;
cleanup();
try { fs.unlinkSync(destPath); } catch (e) { }
reject(error);
});
req.end();
});
}
+16 -9
View File
@@ -17,6 +17,16 @@ const log = (msg) => console.log(`[postinstall] ${msg}`);
const warn = (msg) => console.warn(`[postinstall] ⚠️ ${msg}`);
const error = (msg) => console.error(`[postinstall] ❌ ${msg}`);
/**
* 补丁文件映射: 源文件名 -> 目标文件名
* 供 preflight.js 自检系统复用
*/
export const CAMOUFOX_PATCHES = {
'camoufox-js@0.8.3.locale.patched.js': 'locale.js',
'camoufox-js@0.8.3.pkgman.patched.js': 'pkgman.js',
'camoufox-js@0.8.3.utils.patched.js': 'utils.js' // SOCKS5 代理修复
};
/**
* 复制 camoufox-js 补丁文件到 node_modules
*/
@@ -33,13 +43,7 @@ function patchCamoufoxJs() {
return;
}
// 补丁文件映射: 源文件名 -> 目标文件名
const patches = {
'camoufox-js@0.8.3.locale.patched.js': 'locale.js',
'camoufox-js@0.8.3.pkgman.patched.js': 'pkgman.js'
};
for (const [srcName, destName] of Object.entries(patches)) {
for (const [srcName, destName] of Object.entries(CAMOUFOX_PATCHES)) {
const srcPath = path.join(patchDir, srcName);
const destPath = path.join(targetDir, destName);
@@ -59,5 +63,8 @@ function patchCamoufoxJs() {
log('补丁应用完成。');
}
// 执行
patchCamoufoxJs();
import { fileURLToPath as _fileURLToPath } from 'url';
const isMainModule = process.argv[1] === _fileURLToPath(import.meta.url);
if (isMainModule) {
patchCamoufoxJs();
}
+2 -2
View File
@@ -317,12 +317,12 @@ export const manifest = {
models: [
{ id: 'gemini-3-pro', codeName: 'gemini-3-pro-preview', imagePolicy: 'optional', type: 'text' },
{ id: 'gemini-2.5-pro', codeName: 'gemini-2.5pro', imagePolicy: 'optional', type: 'text' },
{ id: 'gemini-3-flash-preview', codeName: 'gemini-3-pro-preview', imagePolicy: 'optional', type: 'text' },
{ id: 'gemini-3-flash', codeName: 'gemini-3-pro-preview', imagePolicy: 'optional', type: 'text' },
{ id: 'gemini-2.5-flash', codeName: 'gemini-2.5-flash', imagePolicy: 'optional', type: 'text' },
{ id: 'gemini-3-pro-grounding', codeName: 'gemini-3-pro-preview', imagePolicy: 'optional', type: 'text' },
{ id: 'gemini-2.5-pro-grounding', codeName: 'gemini-2.5-pro', imagePolicy: 'optional', type: 'text' },
{ id: 'gemini-2.5-flash-grounding', codeName: 'gemini-2.5-flash', imagePolicy: 'optional', type: 'text' },
{ id: 'gemini-3-flash-preview-grounding', codeName: 'gemini-3-flash-preview', imagePolicy: 'optional', type: 'text' },
{ id: 'gemini-3-flash-grounding', codeName: 'gemini-3-flash-preview', imagePolicy: 'optional', type: 'text' },
],
// 导航处理器
+9 -2
View File
@@ -226,12 +226,20 @@ async function waitForElementStable(element, stableFrames = 10, timeout = 2000)
*/
export async function safeClick(page, target, options = {}) {
const clickCount = options.clickCount || 1;
const timeout = options.timeout || TIMEOUTS.ELEMENT_CLICK;
const waitStable = options.waitStable !== false; // 默认 true
const selector = typeof target === 'string' ? target : '元素';
// humanizeCursorMode: false=禁用, true=ghost-cursor, "camou"=Camoufox内置
// 只有 true 时才使用 ghost-cursor,其他情况都使用原生点击
const useGhostCursor = page?._humanizeCursorMode === true && page?.cursor;
const cursorSpeed = options.cursorSpeed ?? 40;
// 动态计算超时时间:使用 ghost-cursor 时,速度越慢超时越长
// 公式:基础超时 + 额外时间(50000ms / 速度)
// 速度40时额外1.25s,速度10时额外5s,速度5时额外10s
const baseTimeout = options.timeout || TIMEOUTS.ELEMENT_CLICK;
const timeout = useGhostCursor
? baseTimeout + Math.ceil(50000 / cursorSpeed)
: baseTimeout;
const doClick = async () => {
let el;
@@ -270,7 +278,6 @@ export async function safeClick(page, target, options = {}) {
if (box) {
const { x, y } = getHumanClickPoint(box, options.bias || 'random');
logger.debug('浏览器', `[safeClick] 移动鼠标到 (${x.toFixed(0)}, ${y.toFixed(0)})...`);
const cursorSpeed = options.cursorSpeed ?? 40;
await page.cursor.moveTo({ x, y }, { moveSpeed: cursorSpeed });
logger.debug('浏览器', `[safeClick] 执行点击...`);
await page.mouse.click(x, y, { clickCount });
+2 -2
View File
@@ -91,9 +91,9 @@ export class Worker {
logger.info('工作池', `[${this.name}] 正在初始化浏览器...`);
if (this.proxyConfig) {
logger.debug('工作池', `[${this.name}] 使用代理: ${this.proxyConfig.type}://${this.proxyConfig.host}:${this.proxyConfig.port}`);
logger.info('工作池', `[${this.name}] 使用代理: ${this.proxyConfig.type}://${this.proxyConfig.host}:${this.proxyConfig.port}`);
} else {
logger.debug('工作池', `[${this.name}] 直连模式(无代理)`);
logger.info('工作池', `[${this.name}] 直连模式(无代理)`);
}
if (sharedBrowser) {
+3 -7
View File
@@ -8,6 +8,7 @@ import path from 'path';
import os from 'os';
import crypto from 'crypto';
import { logger } from '../utils/logger.js';
import { CAMOUFOX_PATCHES } from '../../scripts/postinstall.js';
const PROJECT_ROOT = process.cwd();
@@ -53,16 +54,11 @@ export function preflight() {
errors.push('better-sqlite3 预编译文件缺失,请运行: npm run init');
}
// 2. 检查 camoufox-js 补丁(通过 MD5 对比)
// 2. 检查 camoufox-js 补丁(通过 MD5 对比,使用 postinstall.js 导出的补丁列表
const patchDir = path.join(PROJECT_ROOT, 'patches');
const targetDir = path.join(PROJECT_ROOT, 'node_modules', 'camoufox-js', 'dist');
const patchFiles = {
'camoufox-js@0.8.3.locale.patched.js': 'locale.js',
'camoufox-js@0.8.3.pkgman.patched.js': 'pkgman.js'
};
for (const [patchName, targetName] of Object.entries(patchFiles)) {
for (const [patchName, targetName] of Object.entries(CAMOUFOX_PATCHES)) {
const patchPath = path.join(patchDir, patchName);
const targetPath = path.join(targetDir, targetName);
+17 -34
View File
@@ -94,44 +94,27 @@ export async function getBrowserProxy(proxyConfig) {
const { type, host, port, user, passwd } = proxyConfig;
// 对于 SOCKS5 + 认证,需要转换为 HTTP 代理
if (type === 'socks5' && user && passwd) {
try {
const originalUrl = buildProxyUrl(proxyConfig);
logger.info('代理器', `检测到需鉴权的 SOCKS5 代理,正在创建本地代理桥接: ${host}:${port}`);
const httpProxyUrl = await anonymizeProxy(originalUrl);
// 保存状态用于后续清理
proxyState.anonymizedProxyUrl = httpProxyUrl;
proxyState.originalProxyUrl = originalUrl;
logger.info('代理器', `本地代理桥接已建立: ${httpProxyUrl} -> ${host}:${port}`);
return {
server: httpProxyUrl
};
} catch (error) {
logger.error('代理器', `本地代理桥接创建失败: ${error.message}`);
throw error;
// 构建代理 URL 字符串
// 注意:Camoufox 对 socks5:// 协议使用 new URL().origin 会返回 null
// 因此我们直接返回完整的 URL 字符串,让 Camoufox 使用 new URL().href
let proxyUrl;
if (user && passwd) {
// 带认证的代理格式: protocol://user:passwd@host:port
const protocol = type === 'socks5' ? 'socks5' : 'http';
proxyUrl = `${protocol}://${encodeURIComponent(user)}:${encodeURIComponent(passwd)}@${host}:${port}`;
} else {
// 不带认证的代理格式: protocol://host:port
if (type === 'socks5') {
proxyUrl = `socks5://${host}:${port}`;
} else {
proxyUrl = `http://${host}:${port}`;
}
}
// 对于其他情况(HTTP 代理、不带认证的 SOCKS5
const proxyUrl = type === 'socks5' ? `socks5://${host}:${port}` : `${host}:${port}`;
logger.info('代理器', `代理配置: ${type}://${host}:${port}${user ? ' (带认证)' : ''}`);
const proxyObject = {
server: proxyUrl
};
// 如果有认证信息,添加到代理对象
if (user && passwd) {
proxyObject.username = user;
proxyObject.password = passwd;
}
logger.info('代理器', `代理配置: ${type}://${host}:${port}`);
return proxyObject;
// 直接返回字符串格式,Camoufox 会正确解析
return proxyUrl;
}
/**