mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
fix: 修复无需鉴权的 Socks5 代理无法使用的问题
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Generated
+17
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
|
||||
// 导航处理器
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user