mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
508 lines
17 KiB
JavaScript
508 lines
17 KiB
JavaScript
import fs from 'fs';
|
|
import { execSync } from 'child_process';
|
|
import path from 'path';
|
|
import os from 'os';
|
|
import { fileURLToPath } from 'url';
|
|
import { gotScraping } from 'got-scraping';
|
|
import compressing from 'compressing';
|
|
import yaml from 'yaml';
|
|
import { logger } from './logger.js';
|
|
import { getHttpProxy, getProxyConfig } from './proxy.js';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const PROJECT_ROOT = path.join(__dirname, '..', '..');
|
|
const TEMP_DIR = path.join(PROJECT_ROOT, 'data', 'temp');
|
|
const CONFIG_PATH = path.join(PROJECT_ROOT, 'config.yaml');
|
|
|
|
// 确保临时目录存在
|
|
if (!fs.existsSync(TEMP_DIR)) {
|
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
}
|
|
|
|
/**
|
|
* 获取 Node.js ABI 版本
|
|
*/
|
|
function getNodeABI() {
|
|
return process.versions.modules;
|
|
}
|
|
|
|
/**
|
|
* 获取平台信息
|
|
*/
|
|
function getPlatformInfo() {
|
|
const platform = os.platform();
|
|
const arch = os.arch();
|
|
const nodeVersion = process.version;
|
|
const abi = getNodeABI();
|
|
|
|
return { platform, arch, nodeVersion, abi };
|
|
}
|
|
|
|
/**
|
|
* 验证平台支持
|
|
*/
|
|
function validatePlatform(platform, arch) {
|
|
const supported = {
|
|
'win32': ['x64'],
|
|
'darwin': ['x64', 'arm64'],
|
|
'linux': ['x64', 'arm64']
|
|
};
|
|
|
|
if (!supported[platform] || !supported[platform].includes(arch)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 验证 Node.js ABI 版本支持
|
|
*/
|
|
function validateABI(abi) {
|
|
const supportedABIs = [115, 121, 123, 125, 127, 128, 130, 131, 132, 133, 135, 136, 137, 139, 140, 141];
|
|
return supportedABIs.includes(parseInt(abi, 10));
|
|
}
|
|
|
|
/**
|
|
* 下载文件(带进度,流式,支持重试)
|
|
* @param {string} url - 下载地址
|
|
* @param {string} destPath - 目标文件路径
|
|
* @param {string|null} proxyUrl - 代理 URL
|
|
* @param {number} maxRetries - 最大重试次数
|
|
*/
|
|
async function downloadFile(url, destPath, proxyUrl = null, maxRetries = 3) {
|
|
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, 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));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 单次下载尝试(内部函数)
|
|
*/
|
|
async function downloadFileOnce(url, destPath, proxyUrl = null) {
|
|
return new Promise((resolve, reject) => {
|
|
const options = {
|
|
http2: false,
|
|
timeout: {
|
|
request: 900000, // 总请求超时 15 分钟
|
|
read: 180000 // 两次数据接收间隔超时 3 分钟
|
|
},
|
|
retry: {
|
|
limit: 0
|
|
},
|
|
headerGeneratorOptions: null,
|
|
headers: {
|
|
'user-agent': 'Wget/1.21.4 (linux-gnu)',
|
|
'accept': '*/*',
|
|
'accept-encoding': 'identity',
|
|
'connection': 'keep-alive'
|
|
}
|
|
};
|
|
|
|
if (proxyUrl) {
|
|
options.proxyUrl = proxyUrl;
|
|
}
|
|
|
|
const downloadStream = gotScraping.stream(url, options);
|
|
const fileStream = fs.createWriteStream(destPath);
|
|
|
|
let downloadedSize = 0;
|
|
let totalSize = 0;
|
|
let lastLogTime = Date.now();
|
|
|
|
downloadStream.on('response', (response) => {
|
|
totalSize = parseInt(response.headers['content-length'] || '0', 10);
|
|
if (totalSize > 0) {
|
|
logger.info('初始化', `文件大小: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
|
|
}
|
|
});
|
|
|
|
downloadStream.on('data', (chunk) => {
|
|
downloadedSize += chunk.length;
|
|
|
|
// 每秒更新一次进度
|
|
const now = Date.now();
|
|
if (totalSize > 0 && now - lastLogTime > 1000) {
|
|
const percent = ((downloadedSize / totalSize) * 100).toFixed(1);
|
|
const downloadedMB = (downloadedSize / 1024 / 1024).toFixed(2);
|
|
const totalMB = (totalSize / 1024 / 1024).toFixed(2);
|
|
logger.info('初始化', `下载进度: ${percent}% (${downloadedMB}MB / ${totalMB}MB)`);
|
|
lastLogTime = now;
|
|
}
|
|
});
|
|
|
|
downloadStream.on('error', (error) => {
|
|
fileStream.close();
|
|
try {
|
|
fs.unlinkSync(destPath);
|
|
} catch (e) { }
|
|
reject(error);
|
|
});
|
|
|
|
fileStream.on('error', (error) => {
|
|
reject(error);
|
|
});
|
|
|
|
fileStream.on('finish', () => {
|
|
const finalSize = (downloadedSize / 1024 / 1024).toFixed(2);
|
|
|
|
// 验证下载完整性
|
|
if (totalSize > 0 && downloadedSize !== totalSize) {
|
|
const errorMsg = `下载不完整: 预期 ${(totalSize / 1024 / 1024).toFixed(2)} MB, 实际 ${finalSize} MB`;
|
|
logger.error('初始化', errorMsg);
|
|
|
|
// 清理损坏的文件
|
|
try {
|
|
fs.unlinkSync(destPath);
|
|
} catch (e) { }
|
|
|
|
reject(new Error(errorMsg));
|
|
return;
|
|
}
|
|
|
|
logger.info('初始化', `下载完成: ${finalSize} MB`);
|
|
resolve(destPath);
|
|
});
|
|
|
|
downloadStream.pipe(fileStream);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 构建 better-sqlite3 下载 URL
|
|
*/
|
|
function getBetterSqlite3Url(platform, arch, abi) {
|
|
const version = '12.5.0';
|
|
const platformMap = {
|
|
'win32': 'win32',
|
|
'darwin': 'darwin',
|
|
'linux': 'linux'
|
|
};
|
|
|
|
const platformName = platformMap[platform];
|
|
const archName = arch; // x64 或 arm64
|
|
|
|
return `https://github.com/WiseLibs/better-sqlite3/releases/download/v${version}/better-sqlite3-v${version}-node-v${abi}-${platformName}-${archName}.tar.gz`;
|
|
}
|
|
|
|
/**
|
|
* 下载并安装 better-sqlite3
|
|
*/
|
|
async function installBetterSqlite3(platform, arch, abi, proxyUrl) {
|
|
logger.info('初始化', '开始安装 better-sqlite3...');
|
|
|
|
const url = getBetterSqlite3Url(platform, arch, abi);
|
|
const downloadPath = path.join(TEMP_DIR, 'better-sqlite3.tar.gz');
|
|
|
|
// 下载
|
|
await downloadFile(url, downloadPath, proxyUrl);
|
|
|
|
// 解压 .tar.gz 文件
|
|
logger.info('初始化', '正在解压 better-sqlite3...');
|
|
await compressing.tgz.uncompress(downloadPath, TEMP_DIR);
|
|
|
|
// 查找 better_sqlite3.node
|
|
const files = fs.readdirSync(TEMP_DIR, { recursive: true });
|
|
const nodeFile = files.find(f => f.endsWith('better_sqlite3.node'));
|
|
if (!nodeFile) {
|
|
throw new Error('未找到 better_sqlite3.node 文件');
|
|
}
|
|
|
|
// 复制到 node_modules
|
|
const buildDir = path.join(PROJECT_ROOT, 'node_modules', 'better-sqlite3', 'build', 'Release');
|
|
if (!fs.existsSync(buildDir)) {
|
|
fs.mkdirSync(buildDir, { recursive: true });
|
|
}
|
|
|
|
const sourcePath = path.join(TEMP_DIR, nodeFile);
|
|
const destPath = path.join(buildDir, 'better_sqlite3.node');
|
|
fs.copyFileSync(sourcePath, destPath);
|
|
|
|
logger.info('初始化', `better-sqlite3 安装成功: ${destPath}`);
|
|
|
|
// 清理
|
|
fs.unlinkSync(downloadPath);
|
|
// 清理解压后的所有文件
|
|
files.forEach(f => {
|
|
const filePath = path.join(TEMP_DIR, f);
|
|
try {
|
|
if (fs.existsSync(filePath)) {
|
|
const stat = fs.statSync(filePath);
|
|
if (stat.isDirectory()) {
|
|
fs.rmSync(filePath, { recursive: true, force: true });
|
|
} else {
|
|
fs.unlinkSync(filePath);
|
|
}
|
|
}
|
|
} catch (e) { }
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 构建 Camoufox 下载 URL
|
|
*/
|
|
function getCamoufoxUrl(platform, arch) {
|
|
const version = '135.0.1-beta.24';
|
|
const platformMap = {
|
|
'win32': 'win',
|
|
'darwin': 'mac',
|
|
'linux': 'lin'
|
|
};
|
|
|
|
const archMap = {
|
|
'x64': 'x86_64',
|
|
'arm64': 'arm64'
|
|
};
|
|
|
|
const platformName = platformMap[platform];
|
|
const archName = archMap[arch];
|
|
|
|
return `https://github.com/daijro/camoufox/releases/download/v${version}/camoufox-${version}-${platformName}.${archName}.zip`;
|
|
}
|
|
|
|
/**
|
|
* 下载并安装 Camoufox
|
|
*/
|
|
async function installCamoufox(platform, arch, proxyUrl) {
|
|
logger.info('初始化', '开始安装 Camoufox 浏览器...');
|
|
|
|
const url = getCamoufoxUrl(platform, arch);
|
|
const downloadPath = path.join(TEMP_DIR, 'camoufox.zip');
|
|
|
|
// 下载
|
|
await downloadFile(url, downloadPath, proxyUrl);
|
|
|
|
// 解压 .zip 文件到 camoufox 目录
|
|
logger.info('初始化', '正在解压 Camoufox...');
|
|
const camoufoxDir = path.join(PROJECT_ROOT, 'camoufox');
|
|
if (!fs.existsSync(camoufoxDir)) {
|
|
fs.mkdirSync(camoufoxDir, { recursive: true });
|
|
}
|
|
|
|
await compressing.zip.uncompress(downloadPath, camoufoxDir);
|
|
|
|
logger.info('初始化', `Camoufox 安装成功: ${camoufoxDir}`);
|
|
|
|
// 更新 config.yaml
|
|
updateConfigPath(platform, camoufoxDir);
|
|
|
|
// 创建 version.json
|
|
const versionJsonPath = path.join(camoufoxDir, 'version.json');
|
|
const versionData = {
|
|
version: "135.0",
|
|
release: "beta.24"
|
|
};
|
|
fs.writeFileSync(versionJsonPath, JSON.stringify(versionData, null, 2), 'utf8');
|
|
logger.info('初始化', `已生成 version.json: ${versionJsonPath}`);
|
|
|
|
// 清理
|
|
fs.unlinkSync(downloadPath);
|
|
}
|
|
|
|
/**
|
|
* 更新 config.yaml 中的 browser.path
|
|
*/
|
|
function updateConfigPath(platform, camoufoxDir) {
|
|
try {
|
|
const configContent = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
|
|
// 解析为文档对象 (CST)
|
|
const doc = yaml.parseDocument(configContent);
|
|
|
|
// 构造绝对路径
|
|
let browserPath;
|
|
if (platform === 'win32') {
|
|
browserPath = path.join(camoufoxDir, 'camoufox.exe');
|
|
} else if (platform === 'darwin') {
|
|
browserPath = path.join(camoufoxDir, 'Camoufox.app', 'Contents', 'MacOS', 'camoufox');
|
|
} else {
|
|
browserPath = path.join(camoufoxDir, 'camoufox');
|
|
}
|
|
|
|
// 规范化路径分隔符
|
|
browserPath = browserPath.replace(/\\/g, '/');
|
|
|
|
// 安全地更新路径,如果节点不存在则创建
|
|
if (!doc.has('browser')) {
|
|
doc.set('browser', { path: browserPath });
|
|
} else {
|
|
const browserNode = doc.get('browser');
|
|
if (browserNode && typeof browserNode.set === 'function') {
|
|
browserNode.set('path', browserPath);
|
|
} else {
|
|
// 如果 browser 不是对象(理论上不应该发生),强制覆盖
|
|
doc.set('browser', { path: browserPath });
|
|
}
|
|
}
|
|
|
|
// 转回字符串,保留注释
|
|
const updatedYaml = doc.toString();
|
|
fs.writeFileSync(CONFIG_PATH, updatedYaml, 'utf8');
|
|
|
|
logger.info('初始化', `已更新配置文件 browser.path: ${browserPath}`);
|
|
} catch (e) {
|
|
logger.error('初始化', '更新配置文件失败', { error: e.message });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 主流程
|
|
*/
|
|
(async () => {
|
|
try {
|
|
logger.info('初始化', '========================================');
|
|
logger.info('初始化', '依赖初始化脚本启动');
|
|
logger.info('初始化', '========================================');
|
|
|
|
// 显示系统信息
|
|
const { platform, arch, nodeVersion, abi } = getPlatformInfo();
|
|
logger.info('初始化', `操作系统: ${platform}`);
|
|
logger.info('初始化', `芯片架构: ${arch}`);
|
|
logger.info('初始化', `Node.js 版本: ${nodeVersion}`);
|
|
logger.info('初始化', `Node.js ABI 版本: ${abi}`);
|
|
|
|
// 验证平台支持
|
|
if (!validatePlatform(platform, arch)) {
|
|
logger.error('初始化', '不支持的平台!');
|
|
logger.error('初始化', `因该项目使用了 Camoufox 浏览器,没有您设备可用的预编译版本`);
|
|
logger.error('初始化', `支持的平台: Windows x64, macOS x64/arm64, Linux x64/arm64`);
|
|
process.exit(1);
|
|
}
|
|
|
|
logger.info('初始化', '平台支持检查通过');
|
|
|
|
// 验证 ABI 版本支持
|
|
if (!validateABI(abi)) {
|
|
logger.error('初始化', '不支持的 Node.js ABI 版本!');
|
|
logger.error('初始化', `当前 ABI 版本: ${abi}`);
|
|
logger.error('初始化', `支持的 ABI 版本: 115, 121, 123, 125, 127, 128, 130, 131, 132, 133, 135, 136, 137, 139, 140, 141`);
|
|
logger.error('初始化', `建议使用 Node.js 20.10.0 或更高版本`);
|
|
process.exit(1);
|
|
}
|
|
|
|
logger.info('初始化', 'ABI 版本检查通过');
|
|
|
|
// 读取并转换代理配置
|
|
let proxyUrl = null;
|
|
try {
|
|
const configContent = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
const config = yaml.parse(configContent);
|
|
const proxyConfig = getProxyConfig(config);
|
|
if (proxyConfig) {
|
|
proxyUrl = await getHttpProxy(proxyConfig);
|
|
}
|
|
} catch (e) {
|
|
logger.warn('初始化', '无法读取配置文件或转换代理,不使用代理');
|
|
}
|
|
|
|
// 安装 better-sqlite3
|
|
await installBetterSqlite3(platform, arch, abi, proxyUrl);
|
|
|
|
// 安装 Camoufox
|
|
await installCamoufox(platform, arch, proxyUrl);
|
|
|
|
// 修复 Camoufox 环境 (Linux)
|
|
fixCamoufoxEnv();
|
|
|
|
logger.info('初始化', '========================================');
|
|
logger.info('初始化', '所有依赖安装完成!');
|
|
logger.info('初始化', '========================================');
|
|
process.exit(0);
|
|
|
|
} catch (err) {
|
|
logger.error('初始化', '初始化失败', { error: err.message });
|
|
process.exit(1);
|
|
}
|
|
})();
|
|
|
|
/**
|
|
* 自动修复 Linux 下 Camoufox 的路径依赖
|
|
* 目的:建立软链接,欺骗 camoufox-js 以为浏览器安装在默认目录,从而防止自动下载
|
|
*/
|
|
function fixCamoufoxEnv() {
|
|
// 1. 仅在 Linux 下执行
|
|
if (os.platform() !== 'linux') return;
|
|
|
|
logger.info('初始化', '正在检查 Camoufox 环境配置...');
|
|
|
|
// --- 路径配置 ---
|
|
// 假设浏览器存放在项目根目录下的 camoufox 文件夹中
|
|
// 依赖 init.js 中已定义的 PROJECT_ROOT 变量
|
|
const customBrowserDir = path.join(PROJECT_ROOT, 'camoufox');
|
|
|
|
// 官方默认缓存路径: ~/.cache/camoufox
|
|
const defaultCacheDir = path.join(os.homedir(), '.cache');
|
|
const defaultLinkPath = path.join(defaultCacheDir, 'camoufox');
|
|
|
|
// 2. 预检查:确保源文件存在
|
|
if (!fs.existsSync(customBrowserDir)) {
|
|
logger.warn('初始化', `未找到自定义浏览器目录: ${customBrowserDir}`);
|
|
logger.warn('初始化', `请确保已将浏览器解压至项目根目录的 camoufox 文件夹`);
|
|
return;
|
|
}
|
|
|
|
// 3. 检查并修复软链接
|
|
if (fs.existsSync(defaultLinkPath)) {
|
|
const stats = fs.lstatSync(defaultLinkPath);
|
|
if (stats.isSymbolicLink()) {
|
|
const currentTarget = fs.readlinkSync(defaultLinkPath);
|
|
if (currentTarget === customBrowserDir) {
|
|
logger.info('初始化', 'Camoufox 路径映射已就绪');
|
|
return;
|
|
}
|
|
logger.info('初始化', '路径映射不一致,正在更新...');
|
|
fs.unlinkSync(defaultLinkPath);
|
|
} else {
|
|
// 备份旧的实体文件夹
|
|
logger.warn('初始化', `默认路径被占用,正在备份...`);
|
|
fs.renameSync(defaultLinkPath, `${defaultLinkPath}_backup_${Date.now()}`);
|
|
}
|
|
} else {
|
|
if (!fs.existsSync(defaultCacheDir)) {
|
|
fs.mkdirSync(defaultCacheDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
// 4. 创建软链接
|
|
try {
|
|
// 使用 shell 命令创建软链接,比 fs.symlinkSync 在某些 Linux 环境下更可靠
|
|
execSync(`ln -sf "${customBrowserDir}" "${defaultLinkPath}"`);
|
|
|
|
// 验证链接是否有效
|
|
if (fs.existsSync(defaultLinkPath)) {
|
|
const linkTarget = fs.readlinkSync(defaultLinkPath);
|
|
logger.info('初始化', `成功创建路径映射: ${defaultLinkPath} -> ${linkTarget}`);
|
|
} else {
|
|
logger.warn('初始化', '软链接创建后无法访问,可能存在权限问题');
|
|
}
|
|
} catch (e) {
|
|
logger.error('初始化', `创建软链接失败: ${e.message}`);
|
|
}
|
|
}
|