zhilian-auto-hi
Version:
智联招聘自动打招呼工具 - 自动化招聘流程的命令行工具
385 lines (384 loc) • 16.5 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const https = __importStar(require("https"));
const fs_1 = require("fs");
const yauzl = __importStar(require("yauzl"));
const ora_1 = __importDefault(require("ora"));
const logger_1 = __importDefault(require("../logger"));
/**
* 浏览器安装服务
* 负责检测、下载和安装Chromium浏览器
*/
class BrowserInstaller {
constructor() {
// 浏览器存储目录
this.browserDir = path.join(process.cwd(), 'browsers', 'chromium');
// Chromium可执行文件路径(解压后在chrome-win子目录中)
this.chromiumPath = path.join(this.browserDir, 'chrome-win', 'chrome.exe');
// 多个下载源,按优先级排序
this.downloadUrls = [
'https://cdn.playwright.dev/dbazure/download/playwright/builds/chromium/1181/chromium-win64.zip',
'https://playwright.download.prss.microsoft.com/dbazure/download/playwright/builds/chromium/1181/chromium-win64.zip',
'https://cdn.playwright.dev/builds/chromium/1181/chromium-win64.zip'
];
}
/**
* 确保浏览器就绪
* 检查浏览器是否存在,不存在则下载并解压
* @returns Promise<string> 浏览器可执行文件路径
*/
async ensureBrowserReady() {
logger_1.default.info('检查浏览器状态...');
// 检查浏览器是否已存在
const existingBrowserPath = this.checkBrowserExists();
if (existingBrowserPath) {
logger_1.default.info(`发现已安装的浏览器: ${existingBrowserPath}`);
return existingBrowserPath;
}
logger_1.default.info('未发现浏览器,开始下载...');
// 下载浏览器
const zipPath = await this.downloadBrowser();
// 解压浏览器
const browserPath = await this.extractBrowser(zipPath);
logger_1.default.info(`浏览器安装完成: ${browserPath}`);
return browserPath;
}
/**
* 检查浏览器是否存在
* @returns string | null 浏览器路径或null
*/
checkBrowserExists() {
try {
if (fs.existsSync(this.chromiumPath)) {
return this.chromiumPath;
}
return null;
}
catch (error) {
logger_1.default.error(`检查浏览器存在性时出错: ${error instanceof Error ? error.message : String(error)}`);
return null;
}
}
/**
* 从多个CDN源下载浏览器zip文件(支持fallback)
* @returns Promise<string> 下载的zip文件路径
*/
async downloadBrowser() {
const tempDir = path.join(process.cwd(), 'temp');
const zipPath = path.join(tempDir, 'chromium-win64.zip');
// 确保临时目录存在
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// 清理之前可能存在的损坏文件
if (fs.existsSync(zipPath)) {
fs.unlinkSync(zipPath);
}
logger_1.default.info('开始下载浏览器...', '📥');
// 尝试每个下载源
for (let i = 0; i < this.downloadUrls.length; i++) {
const url = this.downloadUrls[i];
const isLastAttempt = i === this.downloadUrls.length - 1;
let spinner = null;
try {
logger_1.default.info(`尝试下载源 ${i + 1}/${this.downloadUrls.length}: ${this.getUrlDomain(url)}`, '🌐');
spinner = (0, ora_1.default)('正在连接下载服务器...').start();
const result = await this.downloadFromUrl(url, zipPath, spinner);
spinner.succeed('下载完成');
logger_1.default.success(`从下载源 ${i + 1} 下载成功!`, '✅');
return result;
}
catch (error) {
if (spinner) {
spinner.fail('下载失败');
}
const errorMessage = error instanceof Error ? error.message : String(error);
if (isLastAttempt) {
logger_1.default.error(`所有下载源均失败,最后错误: ${errorMessage}`);
throw new Error(`所有下载源均失败,最后错误: ${errorMessage}`);
}
else {
logger_1.default.warning(`下载源 ${i + 1} 失败: ${errorMessage},尝试下一个源...`, '⚠️');
// 清理可能的部分下载文件
if (fs.existsSync(zipPath)) {
fs.unlinkSync(zipPath);
}
}
}
}
throw new Error('所有下载源均失败');
}
/**
* 获取URL的域名部分(用于日志显示)
*/
getUrlDomain(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname;
}
catch {
return url;
}
}
/**
* 从指定URL下载文件(处理重定向)
*/
async downloadFromUrl(url, zipPath, spinner, isRedirect = false) {
return new Promise((resolve, reject) => {
const file = (0, fs_1.createWriteStream)(zipPath);
let downloadStarted = false;
const request = https.get(url, (response) => {
// 处理重定向
if (response.statusCode === 302 || response.statusCode === 301) {
const redirectUrl = response.headers.location;
if (redirectUrl) {
spinner.text = '处理重定向...';
file.close();
// 递归处理重定向,标记为重定向调用
this.downloadFromUrl(redirectUrl, zipPath, spinner, true).then(resolve).catch(reject);
}
else {
file.close();
reject(new Error('重定向失败:未找到重定向地址'));
}
return;
}
if (response.statusCode !== 200) {
file.close();
reject(new Error(`下载失败,状态码: ${response.statusCode}`));
return;
}
downloadStarted = true;
this.handleDownloadResponse(file, zipPath, resolve, reject, response, spinner);
});
request.on('error', (error) => {
logger_1.default.error(`下载错误: ${error.message}`);
if (!downloadStarted) {
file.close();
}
reject(error);
});
request.setTimeout(30000, () => {
request.destroy();
file.close();
reject(new Error('下载超时'));
});
});
}
/**
* 处理下载响应
* @private
*/
handleDownloadResponse(file, zipPath, resolve, reject, response, spinner) {
const totalSize = parseInt(response.headers['content-length'] || '0', 10);
let downloadedSize = 0;
let lastUpdateTime = 0;
const updateInterval = 500; // 每500ms更新一次进度,减少闪烁
// 开始下载进度显示
if (totalSize > 0) {
const totalSizeMB = (totalSize / 1024 / 1024).toFixed(1);
spinner.text = `下载中... 0% (0/${totalSizeMB} MB)`;
}
else {
spinner.text = '下载中... 0 MB';
}
response.on('data', (chunk) => {
downloadedSize += chunk.length;
const currentTime = Date.now();
// 限制更新频率,避免闪烁
if (currentTime - lastUpdateTime >= updateInterval) {
if (totalSize > 0) {
const progress = Math.round((downloadedSize / totalSize) * 100);
const downloadedMB = (downloadedSize / 1024 / 1024).toFixed(1);
const totalMB = (totalSize / 1024 / 1024).toFixed(1);
spinner.text = `下载中... ${progress}% (${downloadedMB}/${totalMB} MB)`;
}
else {
const downloadedMB = (downloadedSize / 1024 / 1024).toFixed(1);
spinner.text = `下载中... ${downloadedMB} MB`;
}
lastUpdateTime = currentTime;
}
});
response.pipe(file);
file.on('finish', () => {
file.close(() => {
// 验证下载的文件大小
if (downloadedSize === 0) {
reject(new Error('下载的文件大小为0,下载失败'));
return;
}
const finalSizeMB = (downloadedSize / 1024 / 1024).toFixed(1);
// 显示最终进度
if (totalSize > 0) {
spinner.text = `下载完成 100% (${finalSizeMB}/${(totalSize / 1024 / 1024).toFixed(1)} MB)`;
}
else {
spinner.text = `下载完成 ${finalSizeMB} MB`;
}
logger_1.default.success(`浏览器下载完成!文件大小: ${finalSizeMB} MB`, '✅');
resolve(zipPath);
});
});
file.on('error', (error) => {
file.close();
// 清理失败的下载文件
fs.unlink(zipPath, (unlinkError) => {
if (unlinkError) {
logger_1.default.warning(`清理临时文件失败: ${unlinkError.message}`);
}
});
reject(error);
});
}
/**
* 解压下载的zip文件
* @param zipPath zip文件路径
* @returns Promise<string> 浏览器可执行文件路径
*/
async extractBrowser(zipPath) {
// 确保浏览器目录存在
if (!fs.existsSync(this.browserDir)) {
fs.mkdirSync(this.browserDir, { recursive: true });
}
logger_1.default.info('开始解压浏览器文件...', '📦');
const extractSpinner = (0, ora_1.default)('正在解压文件...').start();
return new Promise((resolve, reject) => {
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
if (err) {
extractSpinner.fail('打开zip文件失败');
logger_1.default.error(`解压错误: ${err.message}`);
reject(err);
return;
}
if (!zipfile) {
extractSpinner.fail('zip文件无效');
reject(new Error('zip文件无效'));
return;
}
let extractedFiles = 0;
let totalFiles = 0;
// 计算总文件数
zipfile.on('entry', () => {
totalFiles++;
});
// 重新开始读取条目进行解压
zipfile.readEntry();
zipfile.on('entry', (entry) => {
const entryPath = entry.fileName;
const fullPath = path.join(this.browserDir, entryPath);
// 跳过目录条目
if (entryPath.endsWith('/')) {
// 确保目录存在
if (!fs.existsSync(fullPath)) {
fs.mkdirSync(fullPath, { recursive: true });
}
zipfile.readEntry();
return;
}
// 确保父目录存在
const parentDir = path.dirname(fullPath);
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true });
}
// 解压文件
zipfile.openReadStream(entry, (err, readStream) => {
if (err) {
extractSpinner.fail('读取zip条目失败');
logger_1.default.error(`解压文件 ${entryPath} 时出错: ${err.message}`);
reject(err);
return;
}
if (!readStream) {
extractSpinner.fail('无法读取zip条目');
reject(new Error(`无法读取zip条目: ${entryPath}`));
return;
}
const writeStream = (0, fs_1.createWriteStream)(fullPath);
readStream.pipe(writeStream);
writeStream.on('finish', () => {
extractedFiles++;
// 更新进度
if (totalFiles > 0) {
const progress = Math.round((extractedFiles / totalFiles) * 100);
extractSpinner.text = `解压进度: ${progress}% (${extractedFiles}/${totalFiles})`;
}
// 继续读取下一个条目
zipfile.readEntry();
});
writeStream.on('error', (writeErr) => {
extractSpinner.fail('写入文件失败');
logger_1.default.error(`写入文件 ${fullPath} 时出错: ${writeErr.message}`);
reject(writeErr);
});
});
});
zipfile.on('end', () => {
extractSpinner.succeed(`解压完成!共解压 ${extractedFiles} 个文件`);
// 清理临时下载文件
this.cleanupTempFile(zipPath);
resolve(this.chromiumPath);
});
zipfile.on('error', (zipErr) => {
extractSpinner.fail('解压过程中出错');
logger_1.default.error(`zip文件处理错误: ${zipErr.message}`);
reject(zipErr);
});
});
});
}
/**
* 清理临时下载文件
* @param filePath 要清理的文件路径
*/
cleanupTempFile(filePath) {
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
logger_1.default.info('临时文件已清理', '🧹');
}
}
catch (error) {
logger_1.default.warning(`清理临时文件失败: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
exports.default = BrowserInstaller;