UNPKG

zhilian-auto-hi

Version:

智联招聘自动打招呼工具 - 自动化招聘流程的命令行工具

385 lines (384 loc) 16.5 kB
"use strict"; 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;