commit da94db91816a433cd2d83d9f1beee1c74fb33c01 Author: foxhui Date: Sun Nov 23 22:43:43 2025 +0800 初次提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04fff5a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +data/ +client.js \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c6f5d5 --- /dev/null +++ b/README.md @@ -0,0 +1,375 @@ +# LMArenaImagenAutomator - 使用文档 + +## 📝 项目简介 + +LMArenaImagenAutomator 是一个基于 Puppeteer 的自动化图像生成工具,通过模拟人类操作与 LMArena 网站交互,提供图像生成服务。项目支持两种运行模式: +- **OpenAI 兼容模式**:提供标准的 OpenAI API 接口,便于集成到现有应用 +- **Queue 队列模式**:使用 Server-Sent Events (SSE) 实时推送生成状态 + +### ✨ 主要特性 + +- 🎭 **拟人化操作**:使用贝塞尔曲线模拟真实鼠标移动轨迹 +- 🤖 **智能输入**:模拟人类打字速度和错误纠正行为 +- 🖼️ **多图支持**:最多支持同时上传 5 张参考图片 +- 🔐 **安全认证**:基于 Bearer Token 的 API 鉴权 +- 📊 **队列管理**:智能任务队列,防止请求过载 +- 🌐 **代理支持**:支持 HTTP 和 SOCKS5 代理配置 + +--- + +## 🚀 快速开始 + +### 系统要求 + +- **Node.js**: 16.0 或更高版本 +- **操作系统**: Windows、Linux 或 macOS +- **浏览器**: Google Chrome 或 Chromium (可选,Puppeteer 会自动下载) + +### 安装步骤 + +1. **克隆项目**(如果从仓库获取)或解压项目文件 + +2. **安装依赖** + ```bash + pnpm install + ``` + +3. **生成 API 密钥** + ```bash + npm run genkey + ``` + 此命令会生成一个安全的随机密钥,请保存并配置到 `config.yaml` 中。 + +--- + +## ⚙️ 配置说明 + +### config.yaml 配置文件 + +配置文件位于项目根目录下的 `config.yaml`,包含以下主要配置项: + +#### 服务器配置 +```yaml +server: + # 运行模式: 'openai' (OpenAI 兼容) 或 'queue' (SSE 队列) + type: queue + # 监听端口 + port: 3000 + # API 鉴权密钥 (使用 npm run genkey 生成) + auth: sk-change-me-to-your-secure-key +``` + +#### 浏览器配置 +```yaml +chrome: + # Chrome 可执行文件路径 (留空使用 Puppeteer 内置版本) + # Windows 示例: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + # Linux 示例: "/usr/bin/chromium" + path: "" + + # 是否启用无头模式 (true = 后台运行,false = 显示浏览器) + headless: false + + # 是否启用 GPU 加速 (无显卡服务器设为 false) + gpu: false +``` + +#### 代理配置 +```yaml +chrome: + proxy: + # 是否启用代理 + enable: false + # 代理类型: 'http' 或 'socks5' + type: http + # 代理服务器地址 + host: 127.0.0.1 + # 代理端口 + port: 7890 + # 代理认证 (可选) + # user: username + # passwd: password +``` + +### 重要配置建议 + +| 配置项 | 建议值 | 说明 | +|-------|--------|------| +| `server.type` | `queue` | 使用队列模式可获得实时状态反馈 | +| `server.auth` | 强密钥 | 务必修改默认值,使用 `npm run genkey` 生成 | +| `chrome.headless` | `false` / `true` | 建议保持非无头模式(true已映射为new模式) | +| `chrome.gpu` | `false` / `true` | 无显卡环境强烈建议关闭 | + +--- + +## 📖 使用方法 + +### 【重要】务必进行的步骤 +- 第一次启动程序时必须关闭无头模式启动(**Linux无界面请看文档结尾**) +- 等待网页加载完毕后登录账号(否则会在使用几次后弹出登录界面阻止操作) +- 点击输入框输入任意内容点发送,等待弹出服务条款和CloudFlare Turnstile验证码 +- 点击验证码并同意条款后再次点击发送,此时可能会弹出reCAPTCHA验证码,若出现也将其通过 +- 后续可改为无头模式运行,但建议使用非无头模式避免频繁触发人机验证码 + + +### 方式一:使用 HTTP API + +**启动服务器** +```bash +npm start +``` + +#### OpenAI 兼容模式 +> [!WARNING] +> 由于模拟真实用户的浏览器操作,一次只能进行一个任务,剩下的将会放入队列中等待,为防止客户端超时影响体验,该模式如果已经有3个任务时后来的任务将会直接返回错误代码,推荐使用Queue队列模式,服务器会向客户端发送心跳包以确保连接存活。 + +**配置文件设置** +```yaml +server: + type: openai + port: 3000 + auth: your-secret-key +``` + +**API 请求示例** +```bash +curl -X POST http://127.0.0.1:3000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-secret-key" \ + -d '{ + "messages": [ + { + "type": "text", + "text": "generate a cat" + } + ] + }' +``` + +**响应格式** +```json +{ + "id": "chatcmpl-1732374740123", + "object": "chat.completion", + "created": 1732374740, + "model": "lmarena-image", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "![generated](data:image/jpeg;base64,/9j/4AAQ...)" + }, + "finish_reason": "stop" + }] +} +``` + +#### Queue 队列模式(SSE)(推荐) + +**配置文件设置** +```yaml +server: + type: queue +``` + +**请求端点** +``` +POST http://127.0.0.1:3000/v1/queue/join +``` + +**SSE 事件类型** + +| 事件类型 | 数据格式 | 说明 | +|---------|---------|------| +| `status` | `{status: "queued", position: 1}` | 任务已入队 | +| `status` | `{status: "processing"}` | 开始处理 | +| `result` | `{status: "completed", image: "base64..."}` | 生成成功 | +| `result` | `{status: "error", msg: "错误信息"}` | 生成失败 | +| `heartbeat` | 时间戳 | 保持连接 | +| `done` | `"[DONE]"` | 流结束 | + +**Node.js 示例代码** +```javascript +import http from 'http'; + +const options = { + hostname: '127.0.0.1', + port: 3000, + path: '/v1/queue/join', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer your-secret-key' + } +}; + +const req = http.request(options, (res) => { + res.on('data', (chunk) => { + const lines = chunk.toString().split('\n'); + for (const line of lines) { + if (line.startsWith('event: ')) { + const event = line.substring(7).trim(); + console.log('事件类型:', event); + } else if (line.startsWith('data: ')) { + const data = JSON.parse(line.substring(6)); + console.log('数据:', data); + } + } + }); +}); + +req.write(JSON.stringify({ + messages: [{ role: "user", content: "a cute cat" }] +})); +req.end(); +``` + +#### 带图片的请求 + +**支持格式**:PNG、JPEG、GIF、WebP +**最大数量**:5 张图片 +**数据格式**:Base64 编码 + +**请求示例** +```json +{ + "messages": [{ + "role": "user", + "content": [ + { + "type": "text", + "text": "make it more colorful" + }, + { + "type": "image_url", + "image_url": { + "url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAA..." + } + } + ] + }] +} +``` + +### 方式二:使用CLI客户端脚本 + +**启动CLI工具** +```bash +npm test +``` +根据指引填写图片路径和提示词即可 + +--- + +## 📁 项目结构 + +``` +lmarena/ +├── server.js # HTTP 服务器 (主入口) +├── config.yaml # 配置文件 +├── package.json # 项目依赖 +├── lib/ +│ ├── lmarena.js # 核心生图逻辑 (Puppeteer 操作) +│ ├── config.js # 配置加载器 +│ ├── genApiKey.js # API 密钥生成工具 +│ └── test.js # 功能测试脚本 +└── data/ + ├── chromeUserData/ # Chrome 用户数据 (自动创建) + └── temp/ # 临时图片存储 +``` + +--- + +## 🔧 常见问题 + +### 浏览器启动失败 + +**问题**: `Error: Failed to launch the browser process` + +**解决方案**: +- 确保已安装 Chrome 或 Chromium +- 检查 `config.yaml` 中的 `chrome.path` 是否正确 +- 尝试删除 `data/chromeUserData` 目录后重新运行 + +### GPU 相关错误 + +**问题**: 无显卡服务器运行时出现 GPU 错误 + +**解决方案**: +- 该报错并不会影响程序运行,但是强烈建议在无显卡的设备上关闭GPU加速 +```yaml +chrome: + gpu: false # 禁用 GPU 加速 +``` + +### 请求被拒绝 (429 Too Many Requests) + +**问题**: 并发请求过多 + +**解决方案**: +- 该问题仅存在于OpenAI兼容模式 +- 当前限制:1 个并发 + 2 个排队 (总计 3 个) +- 修改 `server.js` 中的 `MAX_CONCURRENT` 和 `MAX_QUEUE_SIZE` (不建议,应为大多数客户端HTTP请求是有超时时间的) +- 等待当前任务完成后再提交新任务 + +### reCAPTCHA 验证失败 + +**问题**: 返回 `recaptcha validation failed` + +**解决方案**: +- 这是 LMArena 的人机验证机制 +- 建议: + - 降低请求频率 + - 首次使用时手动完成一次验证 (关闭 headless 模式) + - 使用稳定和纯净的 IP 地址 (可使用 ping0.cc 查询IP地址纯净度) + +### 图像生成超时 + +**问题**: 任务超过 120 秒未完成 + +**解决方案**: +- 检查网络连接是否稳定 +- 某些复杂提示词可能需要更长时间 + +### Linux下关闭无头模式运行 + +**问题**: 在Linux多用户模式下无界面运行浏览器 + +**解决方案**: + +方法一:X11转发(适用于后续无头模式运行) +- 推荐使用WindTerm开启右上角X-Server +- 在会话设置中的X11栏目中改为 “内部X11显示” + +方法二:Xvfb+X11VNC(推荐) +- 使用xvfb创建虚拟显示器运行该程序,并且将虚拟显示器映射到VNC中便于后续管理(因为在后续使用中可能还会弹出reCAPTCHA验证码需要手动通过) +- 创建虚拟显示器并运行程序 (99为屏幕号,冲突可行更改) +``` +xvfb-run --server-num=99 --server-args="-ac -screen 0 1280x720x16" npm start +``` +- 将虚拟显示器映射至VNC +``` +x11vnc -display :99 -localhost -nopw -once -noxdamage -ncache 10 +``` +- 后续可用RealVNC等程序通过5900端口连接(推荐使用SSH隧道转发不将VNC直接暴露在公网,然后VNC连接127.0.0.1) +``` +ssh -L 5900:127.0.0.1:5900 root@服务器IP +``` + +--- + +## 📊 配置建议 +| 资源 | 最低配置 | 推荐配置 | +|------|---------|---------| +| CPU | 1核 | 2核及以上 | +| 内存 | 1GB | 2GB 及以上 | + +参考:经测试可以在Oracle的1G1C免费机Debian环境下运行 + +## 📄 许可证 + +本项目仅供学习和研究使用,请遵守 LMArena.ai 的使用条款。 + +--- + +**感谢使用 LMArena 图像生成服务!** 🎉 diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..0850b3f --- /dev/null +++ b/config.yaml @@ -0,0 +1,30 @@ +server: + # 服务器模式: 'openai' (标准兼容) 或 'queue' (流式队列) + type: queue + # 监听端口 + port: 3000 + # 鉴权密钥 (Bearer Token),请使用 genapikey.js 生成 + auth: sk-change-me-to-your-secure-key + +chrome: + # 浏览器可执行文件路径 (留空则使用Puppeteer默认) + # Windows系统示例 "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + # Linux系统示例 "/usr/bin/chromium" + path: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + # 是否启用无头模式 + headless: false + # 是否启用 GPU (无GPU设备运行请使用false) + gpu: false + # 代理设置 + proxy: + # 是否启用代理 + enable: false + # 代理类型: 'http' 或 'socks5' + type: http + # 代理服务器地址 + host: 127.0.0.1 + # 代理端口 + port: 7890 + # 代理认证 (可选) + # user: username + # passwd: password diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..769881e --- /dev/null +++ b/lib/config.js @@ -0,0 +1,81 @@ +import fs from 'fs'; +import path from 'path'; +import yaml from 'js-yaml'; +import crypto from 'crypto'; + +const CONFIG_PATH = path.join(process.cwd(), 'config.yaml'); + +/** + * 生成随机 API Key + */ +function generateApiKey() { + return 'sk-' + crypto.randomBytes(24).toString('hex'); +} + +/** + * 默认配置模板 + */ +function getDefaultConfig() { + return `# LMArena 配置文件 +# 自动生成于 ${new Date().toLocaleString()} + +server: + # 服务器模式: 'openai' (标准兼容) 或 'queue' (流式队列) + type: queue + # 监听端口 + port: 3000 + # 鉴权 Token (Bearer Token) + auth: ${generateApiKey()} + +chrome: + # 浏览器可执行文件路径 (留空则使用Puppeteer默认) + # Windows系统示例 "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + # Linux系统示例 "/usr/bin/chromium" + # path: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + + # 是否启用无头模式 (true: 后台运行, false: 显示界面) + headless: false + + # 是否启用 GPU (无GPU设备运行请使用false) + gpu: false + + # 代理设置 + proxy: + # 是否启用代理 + enable: false + # 代理类型: http 或 socks5 + type: http + # 代理主机 + host: 127.0.0.1 + # 代理端口 + port: 7890 + # 代理认证 (可选) + # user: username + # passwd: password +`; +} + +/** + * 加载配置,如果不存在则自动创建 + */ +function loadConfig() { + try { + if (!fs.existsSync(CONFIG_PATH)) { + console.log('>>> [Config] 配置文件不存在,正在生成默认配置...'); + const defaultConfig = getDefaultConfig(); + fs.writeFileSync(CONFIG_PATH, defaultConfig, 'utf8'); + console.log(`>>> [Config] 已生成默认配置文件: ${CONFIG_PATH}`); + console.log('>>> [Config] 请注意查看生成的随机 API Key'); + } + + const configFile = fs.readFileSync(CONFIG_PATH, 'utf8'); + const config = yaml.load(configFile); + console.log('>>> [Config] 已加载 config.yaml'); + return config; + } catch (e) { + console.error('>>> [Error] 无法加载或生成配置文件:', e.message); + process.exit(1); + } +} + +export default loadConfig(); diff --git a/lib/genApiKey.js b/lib/genApiKey.js new file mode 100644 index 0000000..769e11b --- /dev/null +++ b/lib/genApiKey.js @@ -0,0 +1,18 @@ +import crypto from 'crypto'; + +/** + * 生成随机 API Key + * 格式: sk- + 32位十六进制字符串 + */ +function generateApiKey() { + const buffer = crypto.randomBytes(16); + const hex = buffer.toString('hex'); + return `sk-${hex}`; +} + +const key = generateApiKey(); +console.log('\n=== API Key 生成器 ==='); +console.log('您的新 API Key 是:'); +console.log('\x1b[32m%s\x1b[0m', key); // 绿色高亮 +console.log('\n请将其复制到 config.yaml 的 server.auth 字段中。'); +console.log('======================\n'); diff --git a/lib/lmarena.js b/lib/lmarena.js new file mode 100644 index 0000000..d1bd23e --- /dev/null +++ b/lib/lmarena.js @@ -0,0 +1,465 @@ +import puppeteer from 'puppeteer'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// --- 配置常量 --- +const CHROME_PATH = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'; +const USER_DATA_DIR = path.join(process.cwd(), 'data', 'chromeUserData'); +const TARGET_URL = 'https://lmarena.ai/c/new?mode=direct&chat-modality=image'; +const TEMP_DIR = path.join(process.cwd(), 'data', 'temp'); + +// 确保临时目录存在 +if (!fs.existsSync(TEMP_DIR)) { + fs.mkdirSync(TEMP_DIR, { recursive: true }); +} + +// --- 辅助工具 --- + +/** + * 生成指定范围内的随机数 + * @param {number} min 最小值 + * @param {number} max 最大值 + * @returns {number} 随机数 + */ +const random = (min, max) => Math.random() * (max - min) + min; + +/** + * 随机休眠一段时间 + * @param {number} min 最小毫秒数 + * @param {number} max 最大毫秒数 + */ +const sleep = (min, max) => new Promise(r => setTimeout(r, Math.floor(random(min, max)))); + +/** + * 根据文件扩展名获取 MIME 类型 + * @param {string} filePath 文件路径 + * @returns {string} MIME 类型 + */ +function getMimeType(filePath) { + const ext = path.extname(filePath).toLowerCase(); + const map = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp' }; + return map[ext] || 'application/octet-stream'; +} + +// --- 核心拟人化算法 (贝塞尔曲线 + 物理模拟) --- + +/** + * 三次贝塞尔曲线计算 + */ +function cubicBezier(t, p0, p1, p2, p3) { + const k = 1 - t; + return k * k * k * p0 + 3 * k * k * t * p1 + 3 * k * t * t * p2 + t * t * t * p3; +} + +/** + * 模拟人类鼠标移动轨迹 + * @param {object} page Puppeteer 页面对象 + * @param {number} startX 起始 X 坐标 + * @param {number} startY 起始 Y 坐标 + * @param {number} targetX 目标 X 坐标 + * @param {number} targetY 目标 Y 坐标 + */ +async function humanMove(page, startX, startY, targetX, targetY) { + const distance = Math.sqrt(Math.pow(targetX - startX, 2) + Math.pow(targetY - startY, 2)); + const steps = Math.floor(Math.max(distance / 8, 15)); + + const offset = distance * 0.4; + // 生成两个随机控制点,使轨迹弯曲 + const cp1X = startX + (targetX - startX) / 3 + random(-offset, offset); + const cp1Y = startY + (targetY - startY) / 3 + random(-offset, offset); + const cp2X = startX + 2 * (targetX - startX) / 3 + random(-offset, offset); + const cp2Y = startY + 2 * (targetY - startY) / 3 + random(-offset, offset); + + for (let i = 0; i <= steps; i++) { + const t = i / steps; + // 缓动函数:起步快,结尾慢,模拟人类肌肉运动 + const easeT = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; + + let x = cubicBezier(easeT, startX, cp1X, cp2X, targetX); + let y = cubicBezier(easeT, startY, cp1Y, cp2Y, targetY); + + // 添加微小的随机抖动 + if (i % 3 === 0) { x += random(-1, 1); y += random(-1, 1); } + await page.mouse.move(x, y); + } +} + +/** + * 安全点击元素(包含拟人化移动和点击) + * @param {object} page Puppeteer 页面对象 + * @param {string} selector CSS 选择器 + */ +async function safeClick(page, selector) { + try { + const el = await page.$(selector); + if (!el) throw new Error(`未找到: ${selector}`); + const box = await el.boundingBox(); + if (!box) throw new Error(`不可见: ${selector}`); + + // 先稍微移动一下当前位置 (增加真实感) + await page.mouse.move(box.x - random(50, 100), box.y - random(50, 100), { steps: 2 }); + + // 目标点击位置在元素内部随机区域 + const targetX = box.x + box.width * random(0.3, 0.7); + const targetY = box.y + box.height * random(0.3, 0.7); + + // 移动鼠标到目标位置 + await humanMove(page, box.x - 50, box.y - 50, targetX, targetY); + + // 模拟点击过程:按下 -> 停顿 -> 抬起 + await sleep(100, 300); + await page.mouse.down(); + await sleep(60, 120); + await page.mouse.up(); + } catch (err) { + throw err; + } +} + +/** + * 模拟人类键盘输入 + * @param {object} page Puppeteer 页面对象 + * @param {string} selector 输入框选择器 + * @param {string} text 要输入的文本 + */ +async function humanType(page, selector, text) { + const el = await page.$(selector); + if (!el) throw new Error(`Element not found: ${selector}`); + + // 智能输入策略 + if (text.length < 50) { + // 短文本:保持拟人化逐字输入 + for (let i = 0; i < text.length; i++) { + const char = text[i]; + // 模拟错字 (5% 概率) + if (Math.random() < 0.05) { + await el.type('x', { delay: random(50, 150) }); + await sleep(100, 300); + await page.keyboard.press('Backspace', { delay: random(50, 100) }); + } + await el.type(char); + // 随机击键间隔 + await sleep(30, 100); + } + } else { + // 长文本:假装打字 -> 停顿 -> 粘贴 + const fakeCount = Math.floor(random(3, 8)); + const fakeText = text.substring(0, fakeCount); + + // 1. 假装打字几个字符 + for (let i = 0; i < fakeText.length; i++) { + await el.type(fakeText[i], { delay: random(30, 100) }); + } + + // 2. 停顿思考 (0.5 - 1秒) + await sleep(500, 1000); + + // 3. 全选删除 (模拟 Ctrl+A -> Backspace) + await page.keyboard.down('Control'); + await page.keyboard.press('A'); + await page.keyboard.up('Control'); + await sleep(100, 300); + await page.keyboard.press('Backspace'); + await sleep(100, 300); + + // 4. 瞬间粘贴全部文本 (模拟 Ctrl+V) + await page.evaluate((sel, content) => { + const input = document.querySelector(sel); + input.focus(); + document.execCommand('insertText', false, content); + }, selector, text); + } +} + +/** + * 粘贴图片到输入框 + * @param {object} page Puppeteer 页面对象 + * @param {string} selector 输入框选择器 + * @param {string[]} filePaths 图片文件路径数组 + */ +async function pasteImages(page, selector, filePaths) { + if (!filePaths || filePaths.length === 0) return; + console.log(`>>> [粘贴] 上传 ${filePaths.length} 张图片...`); + + // 读取图片文件并转换为 Base64 + const filesData = filePaths.map(p => { + const clean = p.replace(/['"]/g, '').trim(); + if (!fs.existsSync(clean)) return null; + return { + base64: fs.readFileSync(clean).toString('base64'), + mime: getMimeType(clean), + filename: path.basename(clean) + }; + }).filter(f => f); + + if (filesData.length === 0) return; + + // 点击输入框以获取焦点 + await safeClick(page, selector); + await sleep(500, 800); + + // 使用 Clipboard API 模拟粘贴事件 + await page.evaluate(async (sel, files) => { + const target = document.querySelector(sel); + const dt = new DataTransfer(); + for (const f of files) { + const bin = atob(f.base64); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); + dt.items.add(new File([arr], f.filename, { type: f.mime })); + } + target.dispatchEvent(new ClipboardEvent('paste', { bubbles: true, clipboardData: dt })); + }, selector, filesData); + + console.log('>>> [粘贴] 完成,等待缩略图...'); + // 等待图片上传和缩略图生成 + await sleep(2500, 4000); +} + +/** + * 从响应文本中提取图片 URL + * @param {string} text 响应文本 + * @returns {string|null} 图片 URL 或 null + */ +function extractImage(text) { + if (!text) return null; + const lines = text.split('\n'); + for (const line of lines) { + if (line.startsWith('a2:')) { + try { + const data = JSON.parse(line.substring(3)); + if (data?.[0]?.image) return data[0].image; + } catch (e) { } + } + } + return null; +} +/** + * 初始化浏览器 + * @param {object} config 配置对象 (包含 chrome 配置) + * @returns {Promise<{browser: object, page: object, client: object, width: number, height: number}>} + */ +async function initBrowser(config) { + console.log('>>> [Browser] 开始初始化浏览器'); + + const chromeConfig = config?.chrome || {}; + const width = Math.floor(random(900, 1100)); + const height = Math.floor(random(500, 700)); + + // 1. 基础参数 + const args = [ + '--no-sandbox', + '--disable-setuid-sandbox', + `--window-size=${width},${height}`, + '--disable-blink-features=AutomationControlled', + '--disable-infobars', + '--test-type', + '--no-zygote', + '--disable-dev-shm-usage' + ]; + + // 2. Headless 模式配置 + let headlessMode = false; + if (chromeConfig.headless) { + headlessMode = 'new'; + args.push('--disable-gl-drawing-for-tests'); + console.log('>>> [Browser] Headless 模式: 启用'); + } else { + console.log('>>> [Browser] Headless 模式: 禁用'); + } + + // 3. GPU 配置 + if (chromeConfig.gpu === false) { + args.push( + '--disable-gpu', + '--use-gl=swiftshader', + '--disable-accelerated-2d-canvas' + ); + console.log('>>> [Browser] GPU 加速: 禁用 (优化兼容性)'); + } else { + console.log('>>> [Browser] GPU 加速: 启用'); + } + + // 4. 代理配置 + if (chromeConfig.proxy && chromeConfig.proxy.enable) { + const { type, host, port } = chromeConfig.proxy; + const proxyUrl = type === 'socks5' ? `socks5://${host}:${port}` : `${host}:${port}`; + args.push(`--proxy-server=${proxyUrl}`); + // 禁用 QUIC (HTTP3) 以确保代理兼容性 + args.push('--disable-quic'); + console.log(`>>> [Browser] 代理配置: ${type}://${host}:${port}`); + } + + const browser = await puppeteer.launch({ + headless: headlessMode, + executablePath: chromeConfig.path || undefined, + userDataDir: USER_DATA_DIR, + defaultViewport: null, + ignoreDefaultArgs: ['--enable-automation'], + args: args + }); + + // 重用第一个标签页 + const pages = await browser.pages(); + const page = pages[0]; + + // 5. 代理认证 + if (chromeConfig.proxy && chromeConfig.proxy.enable && chromeConfig.proxy.user) { + await page.authenticate({ + username: chromeConfig.proxy.user, + password: chromeConfig.proxy.passwd + }); + console.log('>>> [Browser] 代理认证: 已设置'); + } + + // 隐藏 WebDriver 特征 + await page.evaluateOnNewDocument(() => { + Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); + }); + + // 创建 CDP 会话以监听网络请求 + const client = await page.target().createCDPSession(); + await client.send('Network.enable'); + + // --- [行为预热] 建立人机检测信任 --- + console.log('>>> [Browser] 正在连接 LMArena...'); + await page.goto(TARGET_URL, { waitUntil: 'networkidle2' }); + + console.log('>>> [Warmup] 正在随机浏览页面以建立信任...'); + + // 计算屏幕中心点 + const centerX = width / 2; + const centerY = height / 2; + + // 第一次移动:从左上角移动到中心附近 + await humanMove(page, 0, 0, centerX + random(-200, 200), centerY + random(-200, 200)); + await sleep(500, 1000); + + // 模拟滚动行为 + try { + await page.mouse.wheel({ deltaY: random(100, 300) }); + await sleep(800, 1500); + await page.mouse.wheel({ deltaY: -random(50, 100) }); + } catch (e) { } + + // 等待输入框出现 + const textareaSelector = 'textarea'; + await page.waitForSelector(textareaSelector, { timeout: 60000 }); + + // 移动鼠标到输入框 + const box = await (await page.$(textareaSelector)).boundingBox(); + if (box) { + await humanMove(page, centerX, centerY, box.x + box.width / 2, box.y + box.height / 2); + await sleep(500, 1000); + } + + console.log('>>> [Browser] 浏览器初始化完成,系统就绪'); + + return { browser, page, client, width, height }; +} + +/** + * 执行生图任务 + * @param {object} context 浏览器上下文 {page, client, width, height} + * @param {string} prompt 提示词 + * @param {string[]} imgPaths 图片路径数组 + * @returns {Promise<{image?: string, text?: string, error?: string}>} + */ +async function generateImage(context, prompt, imgPaths) { + const { page, client, width, height } = context; + const textareaSelector = 'textarea'; + + try { + // 1. 强制开启新会话 (通过URL跳转) + console.log('>>> [Task] 开启新会话...'); + await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded' }); + + // 等待输入框出现 + await page.waitForSelector(textareaSelector, { timeout: 30000 }); + await sleep(1500, 2500); // 等页面稳一点 + + // 2. 粘贴图片 + if (imgPaths && imgPaths.length > 0) { + await pasteImages(page, textareaSelector, imgPaths); + } else { + // 如果没有图片,也点击一下输入框获取焦点 + await safeClick(page, textareaSelector); + } + + // 3. 输入 Prompt + console.log('>>> [Input] 正在输入提示词...'); + await humanType(page, textareaSelector, prompt); + await sleep(800, 1500); + + // 4. 发送 + const btnSelector = 'button[type="submit"]'; + await safeClick(page, btnSelector); + + console.log('>>> [Wait] 等待生成中...'); + + // 5. 监听网络响应 + let targetRequestId = null; + const result = await new Promise((resolve) => { + const cleanup = () => { + client.off('Network.responseReceived', onRes); + client.off('Network.loadingFinished', onLoad); + }; + const onRes = (e) => { + // 监听流式响应接口 + if (e.response.url.includes('/nextjs-api/stream/')) targetRequestId = e.requestId; + }; + const onLoad = async (e) => { + if (e.requestId === targetRequestId) { + try { + const { body, base64Encoded } = await client.send('Network.getResponseBody', { requestId: targetRequestId }); + const content = base64Encoded ? Buffer.from(body, 'base64').toString('utf8') : body; + + // 检查是否包含 reCAPTCHA 错误 + if (content.includes('recaptcha validation failed')) { + cleanup(); + resolve({ error: 'recaptcha validation failed' }); + return; + } + + const img = extractImage(content); + if (img) { + console.log('>>> [Success] 生图成功'); + cleanup(); + resolve({ image: img }); + } else { + console.log('>>> [Task] AI 返回文本回复:', content.substring(0, 150) + '...'); + cleanup(); + resolve({ text: content }); + } + } catch (err) { + cleanup(); + resolve({ error: err.message }); + } + } + }; + client.on('Network.responseReceived', onRes); + client.on('Network.loadingFinished', onLoad); + + // 超时保护 (120秒) + setTimeout(() => { + cleanup(); + resolve({ error: 'Timeout' }); + }, 120000); + }); + + // 任务结束,像人一样把鼠标移开,防止遮挡或误触 + await humanMove(page, width / 2, height / 2, width - 100, height / 2); + + return result; + + } catch (err) { + console.error('>>> [Error] 生成任务失败:', err.message); + return { error: err.message }; + } +} + +export { initBrowser, generateImage, TEMP_DIR }; diff --git a/lib/test.js b/lib/test.js new file mode 100644 index 0000000..628580b --- /dev/null +++ b/lib/test.js @@ -0,0 +1,66 @@ +import readline from 'readline'; +import config from './config.js'; +import { initBrowser, generateImage } from './lmarena.js'; + +/** + * 创建命令行交互接口 + */ +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +/** + * 封装 readline 为 Promise + * @param {string} query 提示问题 + * @returns {Promise} 用户输入 + */ +const ask = (query) => new Promise((resolve) => rl.question(query, resolve)); + +async function main() { + console.log('>>> [CLI] LMArena CLI 测试工具'); + console.log('>>> [CLI] 正在启动浏览器...'); + + let browserContext; + try { + // 传入配置对象 + browserContext = await initBrowser(config); + console.log('>>> [CLI] 浏览器已就绪。'); + } catch (err) { + console.error('>>> [Error] 浏览器启动失败:', err); + process.exit(1); + } + + while (true) { + console.log('-----------------------------'); + + // 1. 获取图片路径 + const imgInput = await ask('>>> [CLI] 请输入图片路径 (多张用逗号隔开,回车跳过): '); + const imagePaths = imgInput.trim() + ? imgInput.split(',').map(p => p.trim()).filter(p => p) + : []; + + // 2. 获取提示词 + const prompt = await ask('>>> [CLI] 请输入提示词: '); + if (!prompt.trim()) { + console.log('>>> [Error] 提示词不能为空,请重试。'); + continue; + } + + console.log(`>>> [CLI] 开始任务: Prompt="${prompt}", Images=${imagePaths.length}`); + + // 3. 调用生图逻辑 + const result = await generateImage(browserContext, prompt, imagePaths); + + // 4. 显示结果 + if (result.error) { + console.error('>>> [Error]', result.error); + } else if (result.image) { + console.log('>>> [Success] 图片 URL:', result.image); + } else { + console.log('>>> [CLI] AI 使用文本回复:', result.text); + } + } +} + +main(); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..4814efd --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "type": "module", + "scripts": { + "start": "node server.js", + "test": "node lib/test.js", + "genkey": "node lib/genApiKey.js" + }, + "dependencies": { + "got-scraping": "^4.1.2", + "js-yaml": "^4.1.1", + "puppeteer": "^24.31.0", + "sharp": "^0.34.5" + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..0b5b868 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1505 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + got-scraping: + specifier: ^4.1.2 + version: 4.1.2 + js-yaml: + specifier: ^4.1.1 + version: 4.1.1 + puppeteer: + specifier: ^24.31.0 + version: 24.31.0 + sharp: + specifier: ^0.34.5 + version: 0.34.5 + +packages: + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@puppeteer/browsers@2.10.13': + resolution: {integrity: sha512-a9Ruw3j3qlnB5a/zHRTkruppynxqaeE4H9WNj5eYGRWqw0ZauZ23f4W2ARf3hghF5doozyD+CRtt7XSYuYRI/Q==} + engines: {node: '>=18'} + hasBin: true + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@sindresorhus/is@5.6.0': + resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} + engines: {node: '>=14.16'} + + '@sindresorhus/is@7.1.1': + resolution: {integrity: sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==} + engines: {node: '>=18'} + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@types/http-cache-semantics@4.0.4': + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + adm-zip@0.5.16: + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + engines: {node: '>=12.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + b4a@1.7.3: + resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.5.1: + resolution: {integrity: sha512-zGUCsm3yv/ePt2PHNbVxjjn0nNB1MkIaR4wOCxJ2ig5pCf5cCVAYJXVhQg/3OhhJV6DB1ts7Hv0oUaElc2TPQg==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.6.2: + resolution: {integrity: sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.7.0: + resolution: {integrity: sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.3.2: + resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==} + + baseline-browser-mapping@2.8.30: + resolution: {integrity: sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==} + hasBin: true + + basic-ftp@5.0.5: + resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} + engines: {node: '>=10.0.0'} + + browserslist@4.28.0: + resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + byte-counter@0.1.0: + resolution: {integrity: sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==} + engines: {node: '>=20'} + + cacheable-lookup@7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + + cacheable-request@13.0.15: + resolution: {integrity: sha512-NjiSrjv37X73FmGGU5ec/M83vWQ6q1Ae3BFe+ABfdeeMy4LOMKYTpfEjrBnLedu43clKZtsYbKrHTIQE7vKq+A==} + engines: {node: '>=18'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + callsites@4.2.0: + resolution: {integrity: sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==} + engines: {node: '>=12.20'} + + caniuse-lite@1.0.30001756: + resolution: {integrity: sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==} + + chromium-bidi@11.0.0: + resolution: {integrity: sha512-cM3DI+OOb89T3wO8cpPSro80Q9eKYJ7hGVXoGS3GkDPxnYSqiv+6xwpIf6XERyJ9Tdsl09hmNmY94BkgZdVekw==} + peerDependencies: + devtools-protocol: '*' + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@10.0.0: + resolution: {integrity: sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==} + engines: {node: '>=20'} + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devtools-protocol@0.0.1521046: + resolution: {integrity: sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==} + + dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + + dot-prop@7.2.0: + resolution: {integrity: sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + electron-to-chromium@1.5.259: + resolution: {integrity: sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + form-data-encoder@4.1.0: + resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} + engines: {node: '>= 18'} + + generative-bayesian-network@2.1.77: + resolution: {integrity: sha512-viU4CRPsmgiklR94LhvdMndaY73BkCH1pGjmOjWbLR/ZwcUd06gKF3TCcsS3npRl74o33YSInSixxm16wIukcA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + + got-scraping@4.1.2: + resolution: {integrity: sha512-LtVwPM5YLnNY7HVT/AK/yDBUg/4yOZSlAjjug2ovrHQseS43QCmO1XosKKXcXrfc6OMX8OnDbAWIauFMcaJ5TQ==} + engines: {node: '>=16'} + + got@14.6.4: + resolution: {integrity: sha512-DjsLab39NUMf5iYlK9asVCkHMhaA2hEhrlmf+qXRhjEivuuBHWYbjmty9DA3OORUwZgENTB+6vSmY2ZW8gFHVw==} + engines: {node: '>=20'} + + header-generator@2.1.77: + resolution: {integrity: sha512-ggSG/mfkFMu8CO7xP591G8kp1IJCBvgXu7M8oxTjC9u914JsIzE6zIfoFsXzA+pf0utWJhUsdqU0oV/DtQ4DFQ==} + engines: {node: '>=16.0.0'} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http2-wrapper@2.2.1: + 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'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + keyv@5.5.4: + resolution: {integrity: sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lowercase-keys@3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + mimic-response@4.0.0: + resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-url@8.1.0: + resolution: {integrity: sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==} + engines: {node: '>=14.16'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + ow@0.28.2: + resolution: {integrity: sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==} + engines: {node: '>=12'} + + ow@1.1.1: + resolution: {integrity: sha512-sJBRCbS5vh1Jp9EOgwp1Ws3c16lJrUkJYlvWTYC03oyiYVwS/ns7lKRWow4w4XjDyTrA2pplQv4B2naWSR6yDA==} + engines: {node: '>=14.16'} + + p-cancelable@4.0.1: + resolution: {integrity: sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==} + engines: {node: '>=14.16'} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + puppeteer-core@24.31.0: + resolution: {integrity: sha512-pnAohhSZipWQoFpXuGV7xCZfaGhqcBR9C4pVrU0QSrcMi7tQMH9J9lDBqBvyMAHQqe8HCARuREqFuVKRQOgTvg==} + engines: {node: '>=18'} + + puppeteer@24.31.0: + resolution: {integrity: sha512-q8y5yLxLD8xdZdzNWqdOL43NbfvUOp60SYhaLZQwHC9CdKldxQKXOyJAciOr7oUJfyAH/KgB2wKvqT2sFKoVXA==} + engines: {node: '>=18'} + hasBin: true + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + quick-lru@7.3.0: + resolution: {integrity: sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==} + engines: {node: '>=18'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + responselike@4.0.2: + resolution: {integrity: sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==} + engines: {node: '>=20'} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + streamx@2.23.0: + resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + tar-fs@3.1.1: + resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typed-query-selector@2.12.0: + resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + vali-date@1.0.0: + resolution: {integrity: sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==} + engines: {node: '>=0.10.0'} + + webdriver-bidi-protocol@0.3.9: + resolution: {integrity: sha512-uIYvlRQ0PwtZR1EzHlTMol1G0lAlmOe6wPykF9a77AK3bkpvZHzIVxRE2ThOx5vjy2zISe0zhwf5rzuUfbo1PQ==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.28.5': {} + + '@emnapi/runtime@1.7.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.7.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@keyv/serialize@1.1.1': {} + + '@puppeteer/browsers@2.10.13': + dependencies: + debug: 4.4.3 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.5.0 + semver: 7.7.3 + tar-fs: 3.1.1 + yargs: 17.7.2 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/is@4.6.0': {} + + '@sindresorhus/is@5.6.0': {} + + '@sindresorhus/is@7.1.1': {} + + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@types/http-cache-semantics@4.0.4': {} + + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + optional: true + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 24.10.1 + optional: true + + adm-zip@0.5.16: {} + + agent-base@7.1.4: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + + b4a@1.7.3: {} + + bare-events@2.8.2: {} + + bare-fs@4.5.1: + dependencies: + bare-events: 2.8.2 + bare-path: 3.0.0 + bare-stream: 2.7.0(bare-events@2.8.2) + bare-url: 2.3.2 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + bare-os@3.6.2: + optional: true + + bare-path@3.0.0: + dependencies: + bare-os: 3.6.2 + optional: true + + bare-stream@2.7.0(bare-events@2.8.2): + dependencies: + streamx: 2.23.0 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + bare-url@2.3.2: + dependencies: + bare-path: 3.0.0 + optional: true + + baseline-browser-mapping@2.8.30: {} + + basic-ftp@5.0.5: {} + + browserslist@4.28.0: + dependencies: + baseline-browser-mapping: 2.8.30 + caniuse-lite: 1.0.30001756 + electron-to-chromium: 1.5.259 + node-releases: 2.0.27 + update-browserslist-db: 1.1.4(browserslist@4.28.0) + + buffer-crc32@0.2.13: {} + + byte-counter@0.1.0: {} + + cacheable-lookup@7.0.0: {} + + cacheable-request@13.0.15: + dependencies: + '@types/http-cache-semantics': 4.0.4 + get-stream: 9.0.1 + http-cache-semantics: 4.2.0 + keyv: 5.5.4 + mimic-response: 4.0.0 + normalize-url: 8.1.0 + responselike: 4.0.2 + + callsites@3.1.0: {} + + callsites@4.2.0: {} + + caniuse-lite@1.0.30001756: {} + + chromium-bidi@11.0.0(devtools-protocol@0.0.1521046): + dependencies: + devtools-protocol: 0.0.1521046 + mitt: 3.0.1 + zod: 3.25.76 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + cosmiconfig@9.0.0: + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + + data-uri-to-buffer@6.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@10.0.0: + dependencies: + mimic-response: 4.0.0 + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + + detect-libc@2.1.2: {} + + devtools-protocol@0.0.1521046: {} + + dot-prop@6.0.1: + dependencies: + is-obj: 2.0.0 + + dot-prop@7.2.0: + dependencies: + type-fest: 2.19.0 + + electron-to-chromium@1.5.259: {} + + emoji-regex@8.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + escalade@3.2.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fast-fifo@1.3.2: {} + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + form-data-encoder@4.1.0: {} + + generative-bayesian-network@2.1.77: + dependencies: + adm-zip: 0.5.16 + tslib: 2.8.1 + + get-caller-file@2.0.5: {} + + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.0.5 + data-uri-to-buffer: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + got-scraping@4.1.2: + dependencies: + got: 14.6.4 + header-generator: 2.1.77 + http2-wrapper: 2.2.1 + mimic-response: 4.0.0 + ow: 1.1.1 + quick-lru: 7.3.0 + tslib: 2.8.1 + + got@14.6.4: + dependencies: + '@sindresorhus/is': 7.1.1 + byte-counter: 0.1.0 + cacheable-lookup: 7.0.0 + cacheable-request: 13.0.15 + decompress-response: 10.0.0 + form-data-encoder: 4.1.0 + http2-wrapper: 2.2.1 + keyv: 5.5.4 + lowercase-keys: 3.0.0 + p-cancelable: 4.0.1 + responselike: 4.0.2 + type-fest: 4.41.0 + + header-generator@2.1.77: + dependencies: + browserslist: 4.28.0 + generative-bayesian-network: 2.1.77 + ow: 0.28.2 + tslib: 2.8.1 + + http-cache-semantics@4.2.0: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http2-wrapper@2.2.1: + dependencies: + 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 + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + ip-address@10.1.0: {} + + is-arrayish@0.2.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-obj@2.0.0: {} + + is-stream@4.0.1: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-parse-even-better-errors@2.3.1: {} + + keyv@5.5.4: + dependencies: + '@keyv/serialize': 1.1.1 + + lines-and-columns@1.2.4: {} + + lodash.isequal@4.5.0: {} + + lowercase-keys@3.0.0: {} + + lru-cache@7.18.3: {} + + mimic-response@4.0.0: {} + + mitt@3.0.1: {} + + ms@2.1.3: {} + + netmask@2.0.2: {} + + node-releases@2.0.27: {} + + normalize-url@8.1.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + ow@0.28.2: + dependencies: + '@sindresorhus/is': 4.6.0 + callsites: 3.1.0 + dot-prop: 6.0.1 + lodash.isequal: 4.5.0 + vali-date: 1.0.0 + + ow@1.1.1: + dependencies: + '@sindresorhus/is': 5.6.0 + callsites: 4.2.0 + dot-prop: 7.2.0 + lodash.isequal: 4.5.0 + vali-date: 1.0.0 + + p-cancelable@4.0.1: {} + + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + pend@1.2.0: {} + + picocolors@1.1.1: {} + + progress@2.0.3: {} + + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + proxy-from-env@1.1.0: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + puppeteer-core@24.31.0: + dependencies: + '@puppeteer/browsers': 2.10.13 + chromium-bidi: 11.0.0(devtools-protocol@0.0.1521046) + debug: 4.4.3 + devtools-protocol: 0.0.1521046 + typed-query-selector: 2.12.0 + webdriver-bidi-protocol: 0.3.9 + ws: 8.18.3 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - react-native-b4a + - supports-color + - utf-8-validate + + puppeteer@24.31.0: + dependencies: + '@puppeteer/browsers': 2.10.13 + chromium-bidi: 11.0.0(devtools-protocol@0.0.1521046) + cosmiconfig: 9.0.0 + devtools-protocol: 0.0.1521046 + puppeteer-core: 24.31.0 + typed-query-selector: 2.12.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - react-native-b4a + - supports-color + - typescript + - utf-8-validate + + quick-lru@5.1.1: {} + + quick-lru@7.3.0: {} + + require-directory@2.1.1: {} + + resolve-alpn@1.2.1: {} + + resolve-from@4.0.0: {} + + responselike@4.0.2: + dependencies: + lowercase-keys: 3.0.0 + + semver@7.7.3: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + source-map@0.6.1: + optional: true + + streamx@2.23.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + tar-fs@3.1.1: + dependencies: + pump: 3.0.3 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 4.5.1 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar-stream@3.1.7: + dependencies: + b4a: 1.7.3 + fast-fifo: 1.3.2 + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + text-decoder@1.2.3: + dependencies: + b4a: 1.7.3 + transitivePeerDependencies: + - react-native-b4a + + tslib@2.8.1: {} + + type-fest@2.19.0: {} + + type-fest@4.41.0: {} + + typed-query-selector@2.12.0: {} + + undici-types@7.16.0: + optional: true + + update-browserslist-db@1.1.4(browserslist@4.28.0): + dependencies: + browserslist: 4.28.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + vali-date@1.0.0: {} + + webdriver-bidi-protocol@0.3.9: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.18.3: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + zod@3.25.76: {} diff --git a/server.js b/server.js new file mode 100644 index 0000000..663aa29 --- /dev/null +++ b/server.js @@ -0,0 +1,316 @@ +import http from 'http'; +import fs from 'fs'; +import path from 'path'; +import sharp from 'sharp'; +import { gotScraping } from 'got-scraping'; +import config from './lib/config.js'; +import { initBrowser, generateImage, TEMP_DIR } from './lib/lmarena.js'; + +const PORT = config.server.port || 3000; +const AUTH_TOKEN = config.server.auth; +const SERVER_MODE = config.server.type || 'openai'; // 'openai' 或 'queue' + +// --- 全局状态 --- +let browserContext = null; // 浏览器上下文 {browser, page, client, width, height} +const queue = []; // 请求队列 +let processingCount = 0; // 当前正在处理的任务数 +const MAX_CONCURRENT = 1; // 同时处理的任务数 (Puppeteer 只能单线程操作) +const MAX_QUEUE_SIZE = 2; // 最大排队数 (总容量 = MAX_CONCURRENT + MAX_QUEUE_SIZE = 3) + +/** + * 处理队列中的任务 + */ +async function processQueue() { + // 如果正在处理的任务已满,或队列为空,则停止 + if (processingCount >= MAX_CONCURRENT || queue.length === 0) return; + + // 取出下一个任务 + const task = queue.shift(); + processingCount++; + + // 如果是 Queue 模式,通知客户端状态变更 + if (SERVER_MODE === 'queue' && task.sse) { + task.sse.send('status', { status: 'processing' }); + } + + try { + console.log(`>>> [Queue] 开始处理任务。剩余排队: ${queue.length}`); + + // 确保浏览器已初始化 + if (!browserContext) { + browserContext = await initBrowser(config); + } + + const { req, res, prompt, imagePaths } = task; + + // 调用核心生图逻辑 + const result = await generateImage(browserContext, prompt, imagePaths); + + // 清理临时图片 + for (const p of imagePaths) { + try { fs.unlinkSync(p); } catch (e) { } + } + + // 处理结果 + let finalContent = ''; + let queueResult = {}; + + if (result.error) { + // 特殊错误处理:reCAPTCHA + if (result.error === 'recaptcha validation failed') { + if (SERVER_MODE === 'openai') { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'recaptcha validation failed' })); + } else { + task.sse.send('result', { status: 'error', image: null, msg: 'recaptcha validation failed' }); + task.sse.send('done', '[DONE]'); + task.sse.end(); + } + return; + } + finalContent = `[生成错误] ${result.error}`; + queueResult = { status: 'error', image: null, msg: result.error }; + } else if (result.image) { + try { + console.log('>>> [Download] 正在下载生成结果...'); + const response = await gotScraping({ + url: result.image, + responseType: 'buffer', + http2: true, + headerGeneratorOptions: { + browsers: [{ name: 'chrome', minVersion: 110 }], + devices: ['desktop'], + locales: ['en-US'], + operatingSystems: ['windows'], + } + }); + const imgBuffer = response.body; + + // 检测图片格式并转 Base64 + const metadata = await sharp(imgBuffer).metadata(); + const mimeType = metadata.format === 'png' ? 'image/png' : 'image/jpeg'; + const base64 = imgBuffer.toString('base64'); + + finalContent = `![generated](data:${mimeType};base64,${base64})`; + queueResult = { status: 'completed', image: base64, msg: '' }; + console.log('>>> [Response] 图片已转换为 Base64'); + } catch (e) { + console.error('>>> [Error] 图片下载失败:', e.message); + finalContent = `[图片下载失败] ${result.image}`; + queueResult = { status: 'error', image: null, msg: `Download failed: ${e.message}` }; + } + } else { + finalContent = result.text || '生成失败'; + queueResult = { status: 'completed', image: null, msg: result.text }; + } + + // 发送响应 + if (SERVER_MODE === 'openai') { + const response = { + id: 'chatcmpl-' + Date.now(), + object: 'chat.completion', + created: Math.floor(Date.now() / 1000), + model: 'lmarena-image', + choices: [{ + index: 0, + message: { + role: 'assistant', + content: finalContent + }, + finish_reason: 'stop' + }] + }; + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(response)); + } else { + // Queue Mode + task.sse.send('result', queueResult); + task.sse.send('done', '[DONE]'); + task.sse.end(); + } + + } catch (err) { + console.error('>>> [Error] 任务处理失败:', err); + if (SERVER_MODE === 'openai') { + if (!task.res.writableEnded) { + task.res.writeHead(500, { 'Content-Type': 'application/json' }); + task.res.end(JSON.stringify({ error: err.message })); + } + } else { + task.sse.send('result', { status: 'error', image: null, msg: err.message }); + task.sse.send('done', '[DONE]'); + task.sse.end(); + } + } finally { + processingCount--; + // 递归处理下一个任务 + processQueue(); + } +} + +/** + * 启动 HTTP 服务器 + */ +async function startServer() { + // 预先启动浏览器 + try { + browserContext = await initBrowser(config); + } catch (err) { + console.error('>>> [Error] 浏览器初始化失败:', err); + process.exit(1); + } + + const server = http.createServer(async (req, res) => { + // --- 鉴权中间件 --- + const authHeader = req.headers['authorization']; + if (!authHeader || authHeader !== `Bearer ${AUTH_TOKEN}`) { + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Unauthorized' })); + return; + } + + // --- 路由分发 --- + const isQueueMode = SERVER_MODE === 'queue'; + const targetPath = isQueueMode ? '/v1/queue/join' : '/v1/chat/completions'; + + if (req.method === 'POST' && req.url.startsWith(targetPath)) { + // --- SSE 设置 (仅 Queue 模式) --- + let sseHelper = null; + if (isQueueMode) { + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + }); + + sseHelper = { + send: (event, data) => { + res.write(`event: ${event}\n`); + res.write(`data: ${typeof data === 'object' ? JSON.stringify(data) : data}\n\n`); + }, + end: () => res.end() + }; + + // 启动心跳 + const heartbeat = setInterval(() => { + if (res.writableEnded) { + clearInterval(heartbeat); + return; + } + sseHelper.send('heartbeat', Date.now()); + }, 3000); + } + + const chunks = []; + req.on('data', chunk => chunks.push(chunk)); + req.on('end', async () => { + try { + // --- 限流检查 (仅 OpenAI 模式) --- + if (!isQueueMode && processingCount + queue.length >= MAX_CONCURRENT + MAX_QUEUE_SIZE) { + console.warn('>>> [Server] 请求过多,已拒绝(限流)'); + if (isQueueMode) { + sseHelper.send('error', { msg: 'Too Many Requests' }); + sseHelper.end(); + } else { + res.writeHead(429, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Too Many Requests. Server is busy.' })); + } + return; + } + + const body = Buffer.concat(chunks).toString(); + const data = JSON.parse(body); + const messages = data.messages; + + if (!messages || messages.length === 0) { + if (isQueueMode) { sseHelper.send('error', { msg: 'No messages' }); sseHelper.end(); } + else { res.writeHead(400); res.end(JSON.stringify({ error: 'No messages' })); } + return; + } + + // 筛选用户消息 + const userMessages = messages.filter(m => m.role === 'user'); + if (userMessages.length === 0) { + if (isQueueMode) { sseHelper.send('error', { msg: 'No user messages' }); sseHelper.end(); } + else { res.writeHead(400); res.end(JSON.stringify({ error: 'No user messages' })); } + return; + } + const lastMessage = userMessages[userMessages.length - 1]; + + let prompt = ''; + const imagePaths = []; + let imageCount = 0; + + // 解析内容 (拼接文本 + 处理图片) + if (Array.isArray(lastMessage.content)) { + for (const item of lastMessage.content) { + if (item.type === 'text') { + prompt += item.text + ' '; + } else if (item.type === 'image_url' && item.image_url && item.image_url.url) { + imageCount++; + if (imageCount > 5) { + return; + } + + const url = item.image_url.url; + if (url.startsWith('data:image')) { + const matches = url.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/); + if (matches && matches.length === 3) { + const buffer = Buffer.from(matches[2], 'base64'); + // 压缩图片 + const processedBuffer = await sharp(buffer) + .jpeg({ quality: 90 }) + .toBuffer(); + + const filename = `img_${Date.now()}_${Math.random().toString(36).substring(7)}.jpg`; + const filePath = path.join(TEMP_DIR, filename); + fs.writeFileSync(filePath, processedBuffer); + imagePaths.push(filePath); + } + } + } + } + } else { + prompt = lastMessage.content; // 回落保留 + } + + prompt = prompt.trim(); + console.log(`>>> [Queue] 请求入队 - Prompt: ${prompt}, Images: ${imagePaths.length}`); + + if (isQueueMode) { + sseHelper.send('status', { status: 'queued', position: queue.length + 1 }); + } + + // 将任务加入队列 + queue.push({ req, res, prompt, imagePaths, sse: sseHelper }); + + // 触发队列处理 + processQueue(); + + } catch (err) { + console.error('>>> [Error] 服务器处理失败:', err); + if (isQueueMode && sseHelper) { + sseHelper.send('error', { msg: err.message }); + sseHelper.end(); + } else if (!res.writableEnded) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: err.message })); + } + } + }); + } else { + res.writeHead(404); + res.end(); + } + }); + + server.listen(PORT, () => { + console.log(`>>> [Server] HTTP 服务器启动成功,监听端口 ${PORT}`); + console.log(`>>> [Server] 运行模式: ${SERVER_MODE === 'openai' ? 'OpenAI 兼容模式' : 'Queue 队列模式'}`); + if (SERVER_MODE === 'openai') { + console.log(`>>> [Server] 最大并发: ${MAX_CONCURRENT}, 最大排队: ${MAX_QUEUE_SIZE}`); + } + }); +} + +startServer();