feat: 支持多窗口并行且支持账号数据隔离

This commit is contained in:
foxhui
2025-12-14 19:07:20 +08:00
parent d3129d0641
commit 11c768d73f
19 changed files with 1822 additions and 931 deletions
+44 -23
View File
@@ -3,7 +3,6 @@
* @description 通过自动化方式驱动 Gemini 网页端生成图片,并将结果转换为统一的后端返回结构。
*/
import { initBrowserBase } from '../../browser/launcher.js';
import {
sleep,
safeClick,
@@ -19,27 +18,6 @@ import { logger } from '../../utils/logger.js';
// --- 配置常量 ---
const TARGET_URL = 'https://gemini.google.com/app?hl=en';
/**
* 初始化浏览器会话
* @param {object} config - 全局配置对象
* @returns {Promise<{browser: object, page: object, config: object}>}
*/
async function initBrowser(config) {
// 输入框验证逻辑
const waitInputValidator = async (page) => {
await page.getByRole('textbox').waitFor({ timeout: 60000 });
await safeClick(page, page.getByRole('textbox'), { bias: 'input' });
await sleep(500, 1000);
};
const base = await initBrowserBase(config, {
userDataDir: config.paths.userDataDir,
targetUrl: TARGET_URL,
productName: 'Gemini',
waitInputValidator
});
return { ...base, config };
}
/**
* 执行生图任务
@@ -186,4 +164,47 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
}
}
export { initBrowser, generateImage };
/**
* 输入框就绪校验
* @param {import('playwright-core').Page} page
*/
async function waitInputValidator(page) {
await page.getByRole('textbox').waitFor({ timeout: 60000 });
await safeClick(page, page.getByRole('textbox'), { bias: 'input' });
await sleep(500, 1000);
}
/**
* 适配器 manifest
*/
export const manifest = {
id: 'gemini',
displayName: 'Gemini (Consumer)',
// 入口 URL
getTargetUrl(config, workerConfig) {
return TARGET_URL;
},
// 模型列表
models: [
{ id: 'gemini-3-pro-image-preview', imagePolicy: 'optional' }
],
// 模型 ID 解析(直通)
resolveModelId(modelKey) {
const model = this.models.find(m => m.id === modelKey);
return model ? model.id : null;
},
// 输入框就绪校验
waitInput: waitInputValidator,
// 无需导航处理器
navigationHandlers: [],
// 核心生图方法
generateImage
};
export { generateImage };
+38 -40
View File
@@ -3,7 +3,6 @@
* @description 通过自动化方式驱动 Gemini Business 网页端生成图片,并将结果转换为统一的后端返回结构。
*/
import { initBrowserBase } from '../../browser/launcher.js';
import {
sleep,
safeClick,
@@ -146,43 +145,6 @@ async function waitForInputWithAccountChooser(page, options = {}) {
}
}
/**
* 初始化浏览器
* @param {object} config - 配置对象
* @param {object} [config.browser] - Browser 配置
* @param {boolean} [config.browser.headless] - 是否开启 Headless 模式
* @param {string} [config.browser.path] - Browser 可执行文件路径
* @param {object} [config.browser.proxy] - 代理配置
* @param {object} [config.backend] - 后端配置
* @param {object} [config.backend.geminiBiz] - Gemini Biz 配置
* @param {string} config.backend.geminiBiz.entryUrl - Gemini entry URL (必需)
* @returns {Promise<{browser: object, page: object, client: object}>}
*/
async function initBrowser(config) {
// 从配置读取 Gemini Biz entry URL
const backendCfg = config.backend || {};
const geminiCfg = backendCfg.geminiBiz || {};
const targetUrl = geminiCfg.entryUrl;
if (!targetUrl) {
logger.error('适配器', '未找到GeminiBiz的入口URL, 请在配置文件中配置后再启动', meta);
throw new Error('GeminiBiz backend missing entry URL: backend.geminiBiz.entryUrl');
}
// 输入框验证逻辑(使用公共函数)
const waitInputValidator = async (page) => {
await waitForInputWithAccountChooser(page);
};
const base = await initBrowserBase(config, {
userDataDir: config.paths.userDataDir,
targetUrl,
productName: 'Gemini Enterprise Business',
waitInputValidator,
navigationHandler: handleAccountChooser
});
return { ...base, config };
}
/**
* 生成图片
@@ -196,7 +158,8 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
const { page, config } = context;
try {
const targetUrl = config.backend?.geminiBiz?.entryUrl;
// 支持新路径 adapter.gemini_biz.entryUrl,向下兼容旧路径 geminiBiz.entryUrl
const targetUrl = config.backend?.adapter?.gemini_biz?.entryUrl || config.backend?.geminiBiz?.entryUrl;
if (!targetUrl) {
throw new Error('GeminiBiz backend missing entry URL');
@@ -353,4 +316,39 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
}
}
export { initBrowser, generateImage, handleAccountChooser };
export { generateImage };
/**
* 适配器 manifest
*/
export const manifest = {
id: 'gemini_biz',
displayName: 'Gemini Business',
// 入口 URL (从配置读取,支持新旧路径)
getTargetUrl(config, workerConfig) {
return config?.backend?.adapter?.gemini_biz?.entryUrl || config?.backend?.geminiBiz?.entryUrl || null;
},
// 模型列表
models: [
{ id: 'gemini-3-pro-image-preview', imagePolicy: 'optional' }
],
// 模型 ID 解析(直通)
resolveModelId(modelKey) {
const model = this.models.find(m => m.id === modelKey);
return model ? model.id : null;
},
// 输入框就绪校验
async waitInput(page, ctx) {
await waitForInputWithAccountChooser(page);
},
// 导航处理器
navigationHandlers: [handleAccountChooser],
// 核心生图方法
generateImage
};
+81 -25
View File
@@ -3,7 +3,6 @@
* @description 通过自动化方式驱动 LMArena 网页端生成图片(或解析文本),并将结果转换为统一的后端返回结构。
*/
import { initBrowserBase } from '../../browser/launcher.js';
import {
sleep,
safeClick,
@@ -42,28 +41,6 @@ function extractImage(text) {
return null;
}
/**
* 初始化浏览器会话
* @param {object} config - 全局配置对象
* @returns {Promise<{browser: object, page: object, client: object}>} 初始化后的浏览器上下文
*/
async function initBrowser(config) {
// 输入框验证逻辑
const waitInputValidator = async (page) => {
const textareaSelector = 'textarea';
await page.waitForSelector(textareaSelector, { timeout: 60000 });
await safeClick(page, textareaSelector, { bias: 'input' });
await sleep(500, 1000);
};
const base = await initBrowserBase(config, {
userDataDir: config.paths.userDataDir,
targetUrl: TARGET_URL,
productName: 'LMArena',
waitInputValidator
});
return { ...base, config };
}
/**
* 执行生图任务
@@ -159,7 +136,10 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
const img = extractImage(content);
if (img) {
logger.info('适配器', '已获取结果,正在下载图片...', meta);
const result = await downloadImage(img, config);
const result = await downloadImage(img, {
proxyConfig: context.proxyConfig,
userDataDir: context.userDataDir
});
if (result.image) {
logger.info('适配器', '已下载图片,任务完成', meta);
}
@@ -185,4 +165,80 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
}
}
export { initBrowser, generateImage };
/**
* 输入框就绪校验
* @param {import('playwright-core').Page} page
*/
async function waitInputValidator(page) {
const textareaSelector = 'textarea';
await page.waitForSelector(textareaSelector, { timeout: 60000 });
await safeClick(page, textareaSelector, { bias: 'input' });
await sleep(500, 1000);
}
/**
* 适配器 manifest
*/
export const manifest = {
id: 'lmarena',
displayName: 'LMArena',
// 入口 URL
getTargetUrl(config, workerConfig) {
return TARGET_URL;
},
// 模型列表(从 models.js 迁移)
models: [
{ id: 'gemini-3-pro-image-preview-2k', codeName: '019abc10-e78d-7932-b725-7f1563ed8a12', imagePolicy: 'optional' },
{ id: 'gemini-3-pro-image-preview', codeName: '019aa208-5c19-7162-ae3b-0a9ddbb1e16a', imagePolicy: 'optional' },
{ id: 'flux-2-flex', codeName: '019abed6-d96e-7a2b-bf69-198c28bef281', imagePolicy: 'optional' },
{ id: 'gemini-2.5-flash-image-preview', codeName: '0199ef2a-583f-7088-b704-b75fd169401d', imagePolicy: 'optional' },
{ id: 'hunyuan-image-3.0', codeName: '7766a45c-1b6b-4fb8-9823-2557291e1ddd', imagePolicy: 'forbidden' },
{ id: 'flux-2-pro', codeName: '019abcf4-5600-7a8b-864d-9b8ab7ab7328', imagePolicy: 'optional' },
{ id: 'seedream-4.5', codeName: '019abd43-b052-7eec-aa57-e895e45c9723', imagePolicy: 'optional' },
{ id: 'seedream-4-high-res-fal', codeName: '32974d8d-333c-4d2e-abf3-f258c0ac1310', imagePolicy: 'optional' },
{ id: 'wan2.5-t2i-preview', codeName: '019a5050-2875-78ed-ae3a-d9a51a438685', imagePolicy: 'forbidden' },
{ id: 'gpt-image-1', codeName: '6e855f13-55d7-4127-8656-9168a9f4dcc0', imagePolicy: 'optional' },
{ id: 'gpt-image-mini', codeName: '0199c238-f8ee-7f7d-afc1-7e28fcfd21cf', imagePolicy: 'optional' },
{ id: 'mai-image-1', codeName: '1b407d5c-1806-477c-90a5-e5c5a114f3bc', imagePolicy: 'forbidden' },
{ id: 'seedream-3', codeName: 'd8771262-8248-4372-90d5-eb41910db034', imagePolicy: 'forbidden' },
{ id: 'qwen-image-prompt-extend', codeName: '9fe82ee1-c84f-417f-b0e7-cab4ae4cf3f3', imagePolicy: 'forbidden' },
{ id: 'flux-1-kontext-pro', codeName: '28a8f330-3554-448c-9f32-2c0a08ec6477', imagePolicy: 'optional' },
{ id: 'imagen-3.0-generate-002', codeName: '51ad1d79-61e2-414c-99e3-faeb64bb6b1b', imagePolicy: 'forbidden' },
{ id: 'ideogram-v3-quality', codeName: '73378be5-cdba-49e7-b3d0-027949871aa6', imagePolicy: 'forbidden' },
{ id: 'photon', codeName: 'e7c9fa2d-6f5d-40eb-8305-0980b11c7cab', imagePolicy: 'forbidden' },
{ id: 'recraft-v3', codeName: 'b88d5814-1d20-49cc-9eb6-e362f5851661', imagePolicy: 'forbidden' },
{ id: 'lucid-origin', codeName: '5a3b3520-c87d-481f-953c-1364687b6e8f', imagePolicy: 'forbidden' },
{ id: 'gemini-2.0-flash-preview-image-generation', codeName: '69bbf7d4-9f44-447e-a868-abc4f7a31810', imagePolicy: 'optional' },
{ id: 'dall-e-3', codeName: 'bb97bc68-131c-4ea4-a59e-03a6252de0d2', imagePolicy: 'forbidden' },
{ id: 'flux-1-kontext-dev', codeName: 'eb90ae46-a73a-4f27-be8b-40f090592c9a', imagePolicy: 'optional' },
{ id: 'vidu-q2-image', codeName: '019adb32-afa4-749e-9992-39653b52fe13', imagePolicy: 'optional' },
{ id: 'imagen-4.0-fast-generate-001', codeName: 'f44fd4f8-af30-480f-8ce2-80b2bdfea55e', imagePolicy: 'forbidden' },
{ id: 'imagen-4.0-ultra-generate-001', codeName: '019ae6da-6438-7077-9d2d-b311a35645f8', imagePolicy: 'forbidden' },
{ id: 'flux-2-dev', codeName: '019ae6a0-4773-77d5-8ffb-cc35813e063c', imagePolicy: 'optional' },
{ id: 'imagen-4.0-generate-001', codeName: '019ae6da-6788-761a-8253-e0bb2bf2e3a9', imagePolicy: 'forbidden' },
{ id: 'wan2.5-i2i-preview', codeName: '019aeb62-c6ea-788e-88f9-19b1b48325b5', imagePolicy: 'required' },
{ id: 'hunyuan-image-2.1', codeName: 'a9a26426-5377-4efa-bef9-de71e29ad943', imagePolicy: 'forbidden' },
{ id: 'qwen-image-edit', codeName: '995cf221-af30-466d-a809-8e0985f83649', imagePolicy: 'required' },
{ id: 'reve-v1', codeName: '0199e980-ba42-737b-9436-927b6e7ca73e', imagePolicy: 'required' },
{ id: 'reve-fast-edit', codeName: '019a5675-0a56-7835-abdd-1cb9e7870afa', imagePolicy: 'required' }
],
// 模型 ID 解析
resolveModelId(modelKey) {
const model = this.models.find(m => m.id === modelKey);
return model ? model.codeName : null;
},
// 输入框就绪校验
waitInput: waitInputValidator,
// 无需导航处理器
navigationHandlers: [],
// 核心生图方法
generateImage
};
export { generateImage };
+46 -24
View File
@@ -3,7 +3,7 @@
* @description 通过自动化方式驱动 nanobananafree.ai 网页端生成图片,并将结果转换为统一的后端返回结构。
*/
import { initBrowserBase } from '../../browser/launcher.js';
import {
sleep,
safeClick,
@@ -22,28 +22,6 @@ import { logger } from '../../utils/logger.js';
// --- 配置常量 ---
const TARGET_URL = 'https://nanobananafree.ai/';
/**
* 初始化浏览器
* @param {object} config - 配置对象
* @returns {Promise<{browser: object, page: object, client: object}>}
*/
async function initBrowser(config) {
// NanoBananaFree AI 特定的输入框验证逻辑
const waitInputValidator = async (page) => {
const textareaSelector = 'textarea';
await page.waitForSelector(textareaSelector, { timeout: 60000 });
await safeClick(page, textareaSelector, { bias: 'input' });
await sleep(500, 1000);
};
const base = await initBrowserBase(config, {
userDataDir: config.paths.userDataDir,
targetUrl: TARGET_URL,
productName: 'NanoBananaFree AI',
waitInputValidator
});
return { ...base, config };
}
/**
* 执行生图任务
@@ -152,4 +130,48 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
}
}
export { initBrowser, generateImage };
/**
* 输入框就绪校验
* @param {import('playwright-core').Page} page
*/
async function waitInputValidator(page) {
const textareaSelector = 'textarea';
await page.waitForSelector(textareaSelector, { timeout: 60000 });
await safeClick(page, textareaSelector, { bias: 'input' });
await sleep(500, 1000);
}
/**
* 适配器 manifest
*/
export const manifest = {
id: 'nanobananafree_ai',
displayName: 'NanoBananaFree AI',
// 入口 URL
getTargetUrl(config, workerConfig) {
return TARGET_URL;
},
// 模型列表
models: [
{ id: 'gemini-2.5-flash-image', imagePolicy: 'optional' }
],
// 模型 ID 解析(直通)
resolveModelId(modelKey) {
const model = this.models.find(m => m.id === modelKey);
return model ? model.id : null;
},
// 输入框就绪校验
waitInput: waitInputValidator,
// 无需导航处理器
navigationHandlers: [],
// 核心生图方法
generateImage
};
export { generateImage };
+41 -23
View File
@@ -3,7 +3,6 @@
* @description 通过自动化方式驱动 zai.is 网页端生成图片,并将结果转换为统一的后端返回结构。
*/
import { initBrowserBase } from '../../browser/launcher.js';
import {
sleep,
safeClick,
@@ -164,26 +163,6 @@ async function waitForInputWithAuth(page, options = {}) {
}
}
/**
* 初始化浏览器
* @param {object} config - 配置对象
* @returns {Promise<{browser: object, page: object, client: object}>}
*/
async function initBrowser(config) {
// 输入框验证逻辑(使用公共函数)
const waitInputValidator = async (page) => {
await waitForInputWithAuth(page);
};
const base = await initBrowserBase(config, {
userDataDir: config.paths.userDataDir,
targetUrl: TARGET_URL,
productName: 'Zai.is',
waitInputValidator,
navigationHandler: handleDiscordAuth
});
return { ...base, config };
}
/**
* 生成图片
@@ -395,7 +374,10 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
logger.info('适配器', `已提取图片链接: ${imageUrl}`, meta);
// 下载图片
const downloadResult = await downloadImage(imageUrl, config);
const downloadResult = await downloadImage(imageUrl, {
proxyConfig: context.proxyConfig,
userDataDir: context.userDataDir
});
if (downloadResult.error) {
return downloadResult;
}
@@ -418,4 +400,40 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
}
}
export { initBrowser, generateImage, handleDiscordAuth };
/**
* 适配器 manifest
*/
export const manifest = {
id: 'zai_is',
displayName: 'zAI (zai.is)',
// 入口 URL
getTargetUrl(config, workerConfig) {
return TARGET_URL;
},
// 模型列表
models: [
{ id: 'gemini-3-pro-image-preview', imagePolicy: 'optional' },
{ id: 'gemini-2.5-flash-image', imagePolicy: 'optional' }
],
// 模型 ID 解析(直通)
resolveModelId(modelKey) {
const model = this.models.find(m => m.id === modelKey);
return model ? model.id : null;
},
// 输入框就绪校验
async waitInput(page, ctx) {
await waitForInputWithAuth(page);
},
// 导航处理器
navigationHandlers: [handleDiscordAuth],
// 核心生图方法
generateImage
};
export { generateImage };
+104 -232
View File
@@ -1,269 +1,141 @@
/**
* @fileoverview 后端适配器入口
* @description 负责加载配置、准备运行目录(用户数据/临时目录),并根据配置返回“单后端”或“聚合后端”的统一接口。
* @description 基于 Pool 架构统一管理多浏览器实例,提供统一的对外接口。
*
* 对外统一能力:
* - `initBrowser(cfg)`
* - `initBrowser(cfg)` → 初始化 Pool
* - `generateImage(ctx, prompt, imagePaths, modelId, meta)`
* - `resolveModelId(modelKey)` / `getModels()` / `getImagePolicy(modelKey)`
* - `getCookies(workerName, domain)` - 获取指定 Worker 的 Cookies
*/
import fs from 'fs';
import path from 'path';
import { loadConfig } from '../utils/config.js';
import { initBrowserBase } from '../browser/launcher.js';
import * as modelsModule from './models.js';
import { PoolManager } from './pool.js';
import { logger } from '../utils/logger.js';
// 导入适配器
import * as lmarenaBackend from './adapter/lmarena.js';
import * as geminiBackend from './adapter/gemini_biz.js';
import * as geminiConsumerBackend from './adapter/gemini.js';
import * as nanobananafreeBackend from './adapter/nanobananafree_ai.js';
import * as zaiIsBackend from './adapter/zai_is.js';
// --- 集中管理的路径常量 ---
const USER_DATA_DIR = path.join(process.cwd(), 'data', 'camoufoxUserData');
const TEMP_DIR = path.join(process.cwd(), 'data', 'temp');
// 确保必要目录存在
if (!fs.existsSync(USER_DATA_DIR)) {
fs.mkdirSync(USER_DATA_DIR, { recursive: true });
}
// 确保临时目录存在
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true });
}
// 适配器映射表
const ADAPTER_MAP = {
'gemini_biz': geminiBackend,
'gemini': geminiConsumerBackend,
'nanobananafree_ai': nanobananafreeBackend,
'zai_is': zaiIsBackend,
'lmarena': lmarenaBackend
};
// 2. 聚合后端模式实现(跨调用复用全局 Page)
const MergedBackend = {
name: 'merge',
_globalBrowser: null,
_globalPage: null,
_config: null, // 保存配置引用
initBrowser: async (cfg) => {
MergedBackend._config = cfg; // 保存配置
if (MergedBackend._globalPage && !MergedBackend._globalPage.isClosed()) {
return { browser: MergedBackend._globalBrowser, page: MergedBackend._globalPage, config: cfg };
}
const activeTypes = cfg.backend.merge.type || [];
const handlers = [];
// 收集导航处理器
for (const type of activeTypes) {
const adapter = ADAPTER_MAP[type];
if (type === 'gemini_biz' && adapter.handleAccountChooser) handlers.push(adapter.handleAccountChooser);
if (type === 'zai_is' && adapter.handleDiscordAuth) handlers.push(adapter.handleDiscordAuth);
}
// 聚合处理器
const aggregatedHandler = async (page) => {
for (const handler of handlers) {
try { await handler(page); } catch (e) { }
}
};
logger.info('适配器', `[后端聚合] 启动全局浏览器,聚合: ${activeTypes.join(', ')}`);
const base = await initBrowserBase(cfg, {
userDataDir: cfg.paths.userDataDir,
targetUrl: 'about:blank',
productName: '聚合模式',
navigationHandler: aggregatedHandler,
waitInputValidator: async () => { }
});
MergedBackend._globalBrowser = base.browser;
MergedBackend._globalPage = base.page;
return { ...base, config: cfg };
},
generateImage: async (ctx, prompt, paths, modelId, meta) => {
if (!modelId || !modelId.includes('|')) return { error: 'Invalid aggregated model ID' };
const [adapterType, realId] = modelId.split('|');
const adapter = ADAPTER_MAP[adapterType];
if (!adapter) return { error: `Adapter not found: ${adapterType}` };
logger.info('适配器', `[后端聚合] 路由至: ${adapterType}, Model: ${realId}`, meta);
// 构造子上下文:复用全局 Page,但传入当前 config(适配器会读取 config.backend.geminiBiz 等字段)
const subContext = {
...ctx,
page: MergedBackend._globalPage,
config: ctx.config
};
return adapter.generateImage(subContext, prompt, paths, realId, meta);
},
resolveModelId: (modelKey) => {
const types = MergedBackend._config.backend.merge.type;
// 支持 backend/model 格式指定后端 (如 lmarena/seedream-4.5)
if (modelKey.includes('/')) {
const [specifiedType, actualModel] = modelKey.split('/', 2);
if (ADAPTER_MAP[specifiedType] && types.includes(specifiedType)) {
const realId = modelsModule.resolveModelId(specifiedType, actualModel);
if (realId) return `${specifiedType}|${realId}`;
}
return null; // 指定的后端不存在或不在聚合列表中
}
// 按优先级自动匹配
for (const type of types) {
const realId = modelsModule.resolveModelId(type, modelKey);
if (realId) return `${type}|${realId}`;
}
return null;
},
getModels: () => {
const types = MergedBackend._config.backend.merge.type;
const allModels = [];
const seenIds = new Set();
// 1. 添加按优先级自动选择的模型 (去重)
for (const type of types) {
const result = modelsModule.getModelsForBackend(type);
if (result?.data) {
for (const m of result.data) {
if (!seenIds.has(m.id)) {
seenIds.add(m.id);
allModels.push({
...m,
owned_by: type // 标记优先匹配的后端
});
}
}
}
}
// 2. 添加带前缀的模型 (backend/model 格式,用于指定后端)
for (const type of types) {
const result = modelsModule.getModelsForBackend(type);
if (result?.data) {
for (const m of result.data) {
const prefixedId = `${type}/${m.id}`;
allModels.push({
...m,
id: prefixedId,
owned_by: type
});
}
}
}
return { object: 'list', data: allModels };
},
getImagePolicy: (modelKey) => {
const types = MergedBackend._config.backend.merge.type;
// 支持 backend/model 格式
if (modelKey.includes('/')) {
const [specifiedType, actualModel] = modelKey.split('/', 2);
if (ADAPTER_MAP[specifiedType] && types.includes(specifiedType)) {
return modelsModule.getImagePolicy(specifiedType, actualModel);
}
return 'optional';
}
// 按优先级查找
for (const type of types) {
const realId = modelsModule.resolveModelId(type, modelKey);
if (realId) return modelsModule.getImagePolicy(type, modelKey);
}
return 'optional';
},
/**
* 空闲时导航到监控页面(用于自动续签 Cookie)
* @param {object} cfg - 配置对象
* @returns {Promise<void>}
*/
navigateToMonitor: async (cfg) => {
const monitorType = cfg.backend.merge?.monitor;
if (!monitorType) return;
const page = MergedBackend._globalPage;
if (!page || page.isClosed()) return;
// 适配器目标 URL 映射
const TARGET_URLS = {
'zai_is': 'https://zai.is/',
'lmarena': 'https://lmarena.ai/',
'gemini_biz': cfg.backend.geminiBiz?.entryUrl || 'https://aistudio.google.com/',
'gemini': 'https://gemini.google.com/',
'nanobananafree_ai': 'https://nanobananafree.ai/'
};
const targetUrl = TARGET_URLS[monitorType];
if (!targetUrl) {
logger.warn('适配器', `[Monitor] 未知的监控类型: ${monitorType}`);
return;
}
// 检查当前是否已在目标网站
const currentUrl = page.url();
if (currentUrl.includes(new URL(targetUrl).hostname)) {
logger.debug('适配器', `[Monitor] 已在目标网站: ${monitorType}`);
return;
}
logger.info('适配器', `[Monitor] 空闲中,跳转至: ${monitorType} (${targetUrl})`);
try {
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
// 全局 navigationHandler 会自动处理登录
} catch (e) {
logger.warn('适配器', `[Monitor] 跳转失败: ${e.message}`);
}
}
};
// 全局 PoolManager 实例
let poolManager = null;
/**
* 获取后端接口
* @returns {object} 后端统一接口
*/
export function getBackend() {
const config = loadConfig();
// 将路径常量注入 config 对象
// 将临时目录路径注入 config 对象
config.paths = {
userDataDir: USER_DATA_DIR,
tempDir: TEMP_DIR
};
// 单一后端模式实现(基于当前配置构建)
const SingleBackend = {
name: config.backend.type,
return {
name: 'pool',
config,
TEMP_DIR,
/**
* 初始化 Pool
* @param {object} cfg - 配置对象
* @returns {Promise<{poolManager: PoolManager, config: object}>}
*/
initBrowser: async (cfg) => {
const adapter = ADAPTER_MAP[cfg.backend.type] || lmarenaBackend;
return adapter.initBrowser(cfg);
if (poolManager && poolManager.initialized) {
return { poolManager, config: cfg };
}
poolManager = new PoolManager(cfg);
await poolManager.initAll();
return { poolManager, config: cfg };
},
/**
* 生成图片
* @param {object} ctx - 浏览器上下文 (来自 initBrowser 返回)
* @param {string} prompt - 提示词
* @param {string[]} paths - 图片路径
* @param {string} modelId - 模型 ID
* @param {object} meta - 元信息
*/
generateImage: async (ctx, prompt, paths, modelId, meta) => {
const adapter = ADAPTER_MAP[config.backend.type] || lmarenaBackend;
return adapter.generateImage(ctx, prompt, paths, modelId, meta);
if (!poolManager) {
return { error: 'Pool 未初始化' };
}
return await poolManager.generateImage(ctx, prompt, paths, modelId, meta);
},
resolveModelId: (modelKey) => modelsModule.resolveModelId(config.backend.type, modelKey),
getModels: () => modelsModule.getModelsForBackend(config.backend.type),
getImagePolicy: (modelKey) => modelsModule.getImagePolicy(config.backend.type, modelKey)
/**
* 解析模型 ID
* @param {string} modelKey - 模型 key
* @returns {string|null}
*/
resolveModelId: (modelKey) => {
if (!poolManager) {
logger.warn('适配器', 'resolveModelId 调用时 Pool 未初始化');
return null;
}
return poolManager.resolveModelId(modelKey);
},
/**
* 获取模型列表
* @returns {object}
*/
getModels: () => {
if (!poolManager) {
return { object: 'list', data: [] };
}
return poolManager.getModels();
},
/**
* 获取图片策略
* @param {string} modelKey - 模型 key
* @returns {string}
*/
getImagePolicy: (modelKey) => {
if (!poolManager) {
return 'optional';
}
return poolManager.getImagePolicy(modelKey);
},
/**
* 获取 Cookies
* @param {string} [workerName] - Worker 名称
* @param {string} [domain] - 域名
* @returns {Promise<{worker: string, cookies: object[]}>}
*/
getCookies: async (workerName, domain) => {
if (!poolManager) {
throw new Error('Pool 未初始化');
}
return await poolManager.getCookies(workerName, domain);
},
/**
* 触发监控导航(空闲时)
*/
navigateToMonitor: async () => {
if (poolManager) {
await poolManager.navigateToMonitor();
}
},
/**
* 获取 PoolManager 实例
* @returns {PoolManager|null}
*/
getPoolManager: () => poolManager
};
const isMerge = config.backend.merge && config.backend.merge.enable;
const activeBackend = isMerge ? MergedBackend : SingleBackend;
logger.info('适配器', `后端模式: ${isMerge ? '聚合' : '独立'}`);
return { config, TEMP_DIR, ...activeBackend };
}
-261
View File
@@ -1,261 +0,0 @@
/**
* @fileoverview 模型与图片策略映射
* @description 维护各后端的模型列表/别名与图片输入策略,并提供统一的解析与查询方法供服务器层使用。
*/
/**
* 图片输入策略枚举
* - optional:可带可不带(默认)
* - required:必须有参考图
* - forbidden:禁止带图
*/
export const IMAGE_POLICY = {
OPTIONAL: 'optional', // 可带可不带(默认)
REQUIRED: 'required', // 必须有参考图
FORBIDDEN: 'forbidden' // 禁止带图
};
/** LMArena 后端模型配置(modelKey -> 上游 codeName + 图片策略) */
export const LMARENA_MODELS = {
"gemini-3-pro-image-preview-2k": {
codeName: "019abc10-e78d-7932-b725-7f1563ed8a12",
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"gemini-3-pro-image-preview": {
codeName: "019aa208-5c19-7162-ae3b-0a9ddbb1e16a",
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"flux-2-flex": {
codeName: "019abed6-d96e-7a2b-bf69-198c28bef281",
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"gemini-2.5-flash-image-preview": {
codeName: "0199ef2a-583f-7088-b704-b75fd169401d",
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"hunyuan-image-3.0": {
codeName: "7766a45c-1b6b-4fb8-9823-2557291e1ddd",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"flux-2-pro": {
codeName: "019abcf4-5600-7a8b-864d-9b8ab7ab7328",
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"seedream-4.5": {
codeName: "019abd43-b052-7eec-aa57-e895e45c9723",
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"seedream-4-high-res-fal": {
codeName: "32974d8d-333c-4d2e-abf3-f258c0ac1310",
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"wan2.5-t2i-preview": {
codeName: "019a5050-2875-78ed-ae3a-d9a51a438685",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"gpt-image-1": {
codeName: "6e855f13-55d7-4127-8656-9168a9f4dcc0",
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"gpt-image-mini": {
codeName: "0199c238-f8ee-7f7d-afc1-7e28fcfd21cf",
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"mai-image-1": {
codeName: "1b407d5c-1806-477c-90a5-e5c5a114f3bc",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"seedream-3": {
codeName: "d8771262-8248-4372-90d5-eb41910db034",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"qwen-image-prompt-extend": {
codeName: "9fe82ee1-c84f-417f-b0e7-cab4ae4cf3f3",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"flux-1-kontext-pro": {
codeName: "28a8f330-3554-448c-9f32-2c0a08ec6477",
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"imagen-3.0-generate-002": {
codeName: "51ad1d79-61e2-414c-99e3-faeb64bb6b1b",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"ideogram-v3-quality": {
codeName: "73378be5-cdba-49e7-b3d0-027949871aa6",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"photon": {
codeName: "e7c9fa2d-6f5d-40eb-8305-0980b11c7cab",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"recraft-v3": {
codeName: "b88d5814-1d20-49cc-9eb6-e362f5851661",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"lucid-origin": {
codeName: "5a3b3520-c87d-481f-953c-1364687b6e8f",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"gemini-2.0-flash-preview-image-generation": {
codeName: "69bbf7d4-9f44-447e-a868-abc4f7a31810",
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"dall-e-3": {
codeName: "bb97bc68-131c-4ea4-a59e-03a6252de0d2",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"flux-1-kontext-dev": {
codeName: "eb90ae46-a73a-4f27-be8b-40f090592c9a",
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"vidu-q2-image": {
codeName: "019adb32-afa4-749e-9992-39653b52fe13",
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"imagen-4.0-fast-generate-001": {
codeName: "f44fd4f8-af30-480f-8ce2-80b2bdfea55e",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"imagen-4.0-ultra-generate-001": {
codeName: "019ae6da-6438-7077-9d2d-b311a35645f8",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"flux-2-dev": {
codeName: "019ae6a0-4773-77d5-8ffb-cc35813e063c",
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"imagen-4.0-generate-001": {
codeName: "019ae6da-6788-761a-8253-e0bb2bf2e3a9",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"wan2.5-i2i-preview": {
codeName: "019aeb62-c6ea-788e-88f9-19b1b48325b5",
imagePolicy: IMAGE_POLICY.REQUIRED
},
"hunyuan-image-2.1": {
codeName: "a9a26426-5377-4efa-bef9-de71e29ad943",
imagePolicy: IMAGE_POLICY.FORBIDDEN
},
"qwen-image-edit": {
codeName: "995cf221-af30-466d-a809-8e0985f83649",
imagePolicy: IMAGE_POLICY.REQUIRED
},
"reve-v1": {
codeName: "0199e980-ba42-737b-9436-927b6e7ca73e",
imagePolicy: IMAGE_POLICY.REQUIRED
},
"reve-fast-edit": {
codeName: "019a5675-0a56-7835-abdd-1cb9e7870afa",
imagePolicy: IMAGE_POLICY.REQUIRED
}
};
/** Gemini Business 后端模型配置 */
export const GEMINI_BIZ_MODELS = {
"gemini-3-pro-image-preview": {
imagePolicy: IMAGE_POLICY.OPTIONAL
}
};
/** NanoBananaFree AI 后端模型配置 */
export const NANOBANANAFREE_AI_MODELS = {
"gemini-2.5-flash-image": {
imagePolicy: IMAGE_POLICY.OPTIONAL
}
};
/** zai.is 后端模型配置 */
export const ZAI_IS_MODELS = {
"gemini-3-pro-image-preview": {
imagePolicy: IMAGE_POLICY.OPTIONAL
},
"gemini-2.5-flash-image": {
imagePolicy: IMAGE_POLICY.OPTIONAL
}
};
// Gemini 后端模型配置
export const GEMINI_MODELS = {
"gemini-3-pro-image-preview": {
imagePolicy: IMAGE_POLICY.OPTIONAL
}
};
/**
* 获取后端对应的模型配置表
* @param {string} backendName - 后端名称 ('lmarena' 或 'gemini_biz' 或 'nanobananafree_ai')
* @returns {Object} 模型配置对象
* @private
*/
function getModelsConfigForBackend(backendName) {
switch (backendName) {
case 'lmarena':
return LMARENA_MODELS;
case 'gemini_biz':
return GEMINI_BIZ_MODELS;
case 'gemini':
return GEMINI_MODELS;
case 'nanobananafree_ai':
return NANOBANANAFREE_AI_MODELS;
case 'zai_is':
return ZAI_IS_MODELS;
default:
return {};
}
}
/**
* 获取指定后端的模型列表 (OpenAI格式)
* @param {string} backendName - 后端名称
* @returns {Object} OpenAI 格式的模型列表
*/
export function getModelsForBackend(backendName) {
const modelsConf = getModelsConfigForBackend(backendName);
const modelIds = Object.keys(modelsConf);
return {
object: 'list',
data: modelIds.map(id => ({
id,
object: 'model',
created: Math.floor(Date.now() / 1000),
owned_by: backendName,
// 向前端暴露图片策略
image_policy: modelsConf[id].imagePolicy || IMAGE_POLICY.OPTIONAL
}))
};
}
/**
* 解析模型 ID
* @param {string} backendName - 后端名称
* @param {string} modelKey - 请求的模型键
* @returns {string|null} 返回内部使用的 codeName,若模型无效则返回 null
*/
export function resolveModelId(backendName, modelKey) {
const modelsConf = getModelsConfigForBackend(backendName);
const model = modelsConf[modelKey];
if (!model) return null; // 未配置的模型 -> 无效
// 无 codeName 时,退回到模型 ID 本身
return model.codeName || modelKey;
}
/**
* 获取模型的图片策略
* @param {string} backendName - 后端名称
* @param {string} modelKey - 模型键
* @returns {string} 图片策略 ('optional' | 'required' | 'forbidden')
*/
export function getImagePolicy(backendName, modelKey) {
const modelsConf = getModelsConfigForBackend(backendName);
const model = modelsConf[modelKey];
if (!model || !model.imagePolicy) {
return IMAGE_POLICY.OPTIONAL;
}
return model.imagePolicy;
}
+691
View File
@@ -0,0 +1,691 @@
/**
* @fileoverview Worker Pool 管理模块
* @description 实现 Worker 类和 PoolManager 类,负责多浏览器实例的生命周期管理和任务分发。
* 使用 AdapterRegistry 动态加载适配器,无需硬编码适配器列表。
*
* 对外接口:
* - PoolManager.initAll() - 初始化所有 Worker
* - PoolManager.selectWorker(modelId) - 智能选择 Worker
* - PoolManager.generateImage(ctx, prompt, paths, modelId, meta) - 分发生图任务
* - PoolManager.getModels() / resolveModelId() / getImagePolicy() - 模型相关
* - PoolManager.getCookies(instanceName, domain) - 获取指定实例的 Cookies
*/
import fs from 'fs';
import { logger } from '../utils/logger.js';
import { initBrowserBase, createCursor, getRealViewport, clamp, random, sleep } from '../browser/launcher.js';
import { registry } from './registry.js';
/**
* Worker 类 - 封装单个浏览器实例
*/
class Worker {
/**
* @param {object} globalConfig - 全局配置
* @param {object} workerConfig - Worker 配置
*/
constructor(globalConfig, workerConfig) {
this.name = workerConfig.name;
this.type = workerConfig.type;
this.instanceName = workerConfig.instanceName || null;
this.userDataDir = workerConfig.userDataDir;
this.proxyConfig = workerConfig.resolvedProxy;
this.globalConfig = globalConfig;
this.workerConfig = workerConfig;
// Merge 模式专属
this.mergeTypes = workerConfig.mergeTypes || [];
this.mergeMonitor = workerConfig.mergeMonitor || null;
// 运行时状态
this.browser = null;
this.page = null;
this.busyCount = 0;
this.initialized = false;
}
/**
* 初始化浏览器实例
* @param {object} [sharedBrowser] - 可选,共享的浏览器实例
*/
async init(sharedBrowser = null) {
if (this.initialized) return;
// 确保用户数据目录存在
if (!fs.existsSync(this.userDataDir)) {
fs.mkdirSync(this.userDataDir, { recursive: true });
}
const productName = this.type === 'merge'
? `聚合模式 [${this.name}]`
: `${this.type} [${this.name}]`;
// 获取目标 URL (从 AdapterRegistry 动态获取)
let targetUrl = 'about:blank';
if (this.type === 'merge') {
// Merge 模式:使用第一个 mergeType 的 URL
const firstType = this.mergeTypes[0];
targetUrl = registry.getTargetUrl(firstType, this.globalConfig, this.workerConfig) || 'about:blank';
} else {
targetUrl = registry.getTargetUrl(this.type, this.globalConfig, this.workerConfig) || 'about:blank';
}
// 收集导航处理器 (从 AdapterRegistry 动态获取)
const handlers = [];
const typesToHandle = this.type === 'merge' ? this.mergeTypes : [this.type];
for (const type of typesToHandle) {
const typeHandlers = registry.getNavigationHandlers(type);
handlers.push(...typeHandlers);
}
// 聚合导航处理器
const navigationHandler = handlers.length > 0
? async (page) => {
for (const handler of handlers) {
try { await handler(page); } catch (e) { /* ignore */ }
}
}
: null;
// 获取 waitInputValidator (从 AdapterRegistry 动态获取)
let waitInputValidator = null;
if (this.type !== 'merge') {
waitInputValidator = registry.getWaitInput(this.type);
}
logger.info('工作池', `[${this.name}] 正在初始化浏览器...`);
if (this.proxyConfig) {
logger.debug('工作池', `[${this.name}] 使用代理: ${this.proxyConfig.type}://${this.proxyConfig.host}:${this.proxyConfig.port}`);
} else {
logger.debug('工作池', `[${this.name}] 直连模式(无代理)`);
}
// 如果有共享浏览器,创建新标签页;否则启动新浏览器
if (sharedBrowser) {
logger.info('工作池', `[${this.name}] 复用已有浏览器,创建新标签页...`);
this.browser = sharedBrowser;
// sharedBrowser 实际是 BrowserContextCamoufox 使用 launchPersistentContext
this.page = await sharedBrowser.newPage();
// 初始化 ghost-cursor
this.page.cursor = createCursor(this.page);
// 导航到目标 URL
await this.page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
// 注册导航处理器
if (navigationHandler) {
this.page.on('framenavigated', async () => {
try { await navigationHandler(this.page); } catch (e) { /* ignore */ }
});
}
// 随机浏览以建立信任
logger.info('工作池', `[${this.name}] 正在随机浏览页面以建立信任...`);
const vp = await getRealViewport(this.page);
const centerX = vp.width / 2;
const centerY = vp.height / 2;
if (this.page.cursor) {
const targetX = clamp(centerX + random(-200, 200), 10, vp.safeWidth);
const targetY = clamp(centerY + random(-200, 200), 10, vp.safeHeight);
await this.page.cursor.moveTo({ x: targetX, y: targetY });
}
await sleep(500, 1000);
try {
await this.page.mouse.wheel({ deltaY: random(100, 300) });
await sleep(800, 1500);
await this.page.mouse.wheel({ deltaY: -random(50, 100) });
} catch (e) { }
// 等待输入框就绪
if (waitInputValidator) {
await waitInputValidator(this.page);
}
logger.info('工作池', `[${this.name}] 初始化完成`);
} else {
// 启动新浏览器实例
const base = await initBrowserBase(this.globalConfig, {
userDataDir: this.userDataDir,
instanceName: this.instanceName,
proxyConfig: this.proxyConfig
});
this.browser = base.context;
this.page = base.page;
// 初始化 ghost-cursor
this.page.cursor = createCursor(this.page);
// 注册导航处理器
if (navigationHandler) {
this.page.on('framenavigated', async () => {
try { await navigationHandler(this.page); } catch (e) { /* ignore */ }
});
}
// 导航到目标 URL
logger.info('工作池', `[${this.name}] 正在连接目标页面...`);
await this.page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
// 登录模式:挂起等待用户手动登录
const isLoginMode = process.argv.some(arg => arg.startsWith('-login'));
if (isLoginMode) {
logger.info('工作池', `[${this.name}] 登录模式已就绪,请在浏览器中完成登录`);
logger.info('工作池', `[${this.name}] 完成后可直接关闭浏览器窗口或按 Ctrl+C 退出`);
await new Promise(resolve => this.browser.on('close', resolve));
process.exit(0);
}
// 预热行为
logger.info('工作池', `[${this.name}] 正在执行预热操作...`);
const vp = await getRealViewport(this.page);
const centerX = vp.width / 2;
const centerY = vp.height / 2;
if (this.page.cursor) {
const targetX = clamp(centerX + random(-200, 200), 10, vp.safeWidth);
const targetY = clamp(centerY + random(-200, 200), 10, vp.safeHeight);
await this.page.cursor.moveTo({ x: targetX, y: targetY });
}
await sleep(500, 1000);
try {
await this.page.mouse.wheel({ deltaY: random(100, 300) });
await sleep(800, 1500);
await this.page.mouse.wheel({ deltaY: -random(50, 100) });
} catch (e) { }
// 等待输入框就绪
if (waitInputValidator) {
await waitInputValidator(this.page);
}
logger.info('工作池', `[${this.name}] 初始化完成`);
}
this.initialized = true;
}
/**
* 检查是否支持指定模型
* @param {string} modelId - 模型 ID 或 key
* @returns {boolean}
*/
supports(modelId) {
if (this.type === 'merge') {
// Merge 模式:检查所有 mergeTypes
for (const type of this.mergeTypes) {
const resolved = registry.resolveModelId(type, modelId);
if (resolved) return true;
}
// 检查 backend/model 格式
if (modelId.includes('/')) {
const [specifiedType] = modelId.split('/', 2);
return this.mergeTypes.includes(specifiedType);
}
return false;
} else {
// 单一类型:支持 type/model 格式
if (modelId.includes('/')) {
const [specifiedType, actualModel] = modelId.split('/', 2);
if (specifiedType === this.type) {
const resolved = registry.resolveModelId(this.type, actualModel);
return !!resolved;
}
return false;
}
const resolved = registry.resolveModelId(this.type, modelId);
return !!resolved;
}
}
/**
* 解析模型 ID
* @param {string} modelKey - 模型 key
* @returns {{type: string, realId: string}|null}
*/
resolveModelId(modelKey) {
if (this.type === 'merge') {
// 支持 backend/model 格式
if (modelKey.includes('/')) {
const [specifiedType, actualModel] = modelKey.split('/', 2);
if (this.mergeTypes.includes(specifiedType)) {
const realId = registry.resolveModelId(specifiedType, actualModel);
if (realId) return { type: specifiedType, realId };
}
return null;
}
// 按优先级匹配
for (const type of this.mergeTypes) {
const realId = registry.resolveModelId(type, modelKey);
if (realId) return { type, realId };
}
return null;
} else {
// 单一类型:支持 type/model 格式
if (modelKey.includes('/')) {
const [specifiedType, actualModel] = modelKey.split('/', 2);
if (specifiedType === this.type) {
const realId = registry.resolveModelId(this.type, actualModel);
return realId ? { type: this.type, realId } : null;
}
return null;
}
const realId = registry.resolveModelId(this.type, modelKey);
return realId ? { type: this.type, realId } : null;
}
}
/**
* 生成图片
* @param {object} ctx - 浏览器上下文
* @param {string} prompt - 提示词
* @param {string[]} paths - 图片路径
* @param {string} modelId - 模型 ID
* @param {object} meta - 元信息
*/
async generateImage(ctx, prompt, paths, modelId, meta) {
const resolved = this.resolveModelId(modelId);
if (!resolved) {
return { error: `Worker [${this.name}] 不支持模型: ${modelId}` };
}
const { type, realId } = resolved;
const adapter = registry.getAdapter(type);
if (!adapter) {
return { error: `适配器不存在: ${type}` };
}
logger.info('工作池', `[${this.name}] 执行任务 -> ${type}/${realId}`, meta);
// 构造子上下文
const subContext = {
...ctx,
page: this.page,
config: this.globalConfig,
proxyConfig: this.proxyConfig,
userDataDir: this.userDataDir
};
this.busyCount++;
try {
return await adapter.generateImage(subContext, prompt, paths, realId, meta);
} finally {
this.busyCount--;
}
}
/**
* 获取支持的模型列表
* @returns {object[]}
*/
getModels() {
if (this.type === 'merge') {
const allModels = [];
const seenIds = new Set();
// 添加不带前缀的模型 (由系统自动分配适配器)
for (const type of this.mergeTypes) {
const result = registry.getModelsForAdapter(type);
if (result?.data) {
for (const m of result.data) {
if (!seenIds.has(m.id)) {
seenIds.add(m.id);
allModels.push({ ...m, owned_by: 'internal_server' });
}
}
}
}
// 添加带前缀的模型 (指定使用特定适配器)
for (const type of this.mergeTypes) {
const result = registry.getModelsForAdapter(type);
if (result?.data) {
for (const m of result.data) {
allModels.push({
...m,
id: `${type}/${m.id}`,
owned_by: type
});
}
}
}
return allModels;
} else {
// 单一类型:返回不带前缀和带前缀的模型
const result = registry.getModelsForAdapter(this.type);
const models = result?.data || [];
const allModels = [];
// 不带前缀的模型 (系统自动分配)
for (const m of models) {
allModels.push({ ...m, owned_by: 'internal_server' });
}
// 带前缀的模型 (指定适配器)
for (const m of models) {
allModels.push({
...m,
id: `${this.type}/${m.id}`,
owned_by: this.type
});
}
return allModels;
}
}
/**
* 获取图片策略
* @param {string} modelKey - 模型 key
* @returns {string}
*/
getImagePolicy(modelKey) {
if (this.type === 'merge') {
if (modelKey.includes('/')) {
const [specifiedType, actualModel] = modelKey.split('/', 2);
if (this.mergeTypes.includes(specifiedType)) {
return registry.getImagePolicy(specifiedType, actualModel);
}
}
for (const type of this.mergeTypes) {
const realId = registry.resolveModelId(type, modelKey);
if (realId) return registry.getImagePolicy(type, modelKey);
}
return 'optional';
} else {
return registry.getImagePolicy(this.type, modelKey);
}
}
/**
* 导航到监控页面(空闲时)
*/
async navigateToMonitor() {
if (this.type !== 'merge' || !this.mergeMonitor) return;
if (!this.page || this.page.isClosed()) return;
const targetUrl = registry.getTargetUrl(this.mergeMonitor, this.globalConfig, this.workerConfig);
if (!targetUrl) return;
// 检查是否已在目标网站
const currentUrl = this.page.url();
try {
if (currentUrl.includes(new URL(targetUrl).hostname)) return;
} catch (e) { return; }
logger.info('工作池', `[${this.name}] 空闲,跳转监控: ${this.mergeMonitor}`);
try {
await this.page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
} catch (e) {
logger.warn('工作池', `[${this.name}] 监控跳转失败: ${e.message}`);
}
}
/**
* 获取 Cookies
* @param {string} [domain] - 指定域名
* @returns {Promise<object[]>}
*/
async getCookies(domain) {
if (!this.page) throw new Error(`Worker [${this.name}] 未初始化`);
const context = this.page.context();
if (domain) {
return await context.cookies(domain.startsWith('http') ? domain : `https://${domain}`);
}
return await context.cookies();
}
}
/**
* PoolManager 类 - 管理 Worker 池
*/
class PoolManager {
/**
* @param {object} config - 全局配置
*/
constructor(config) {
this.config = config;
this.workers = [];
this.strategy = config.backend.pool.strategy || 'least_busy';
this.roundRobinIndex = 0;
this.initialized = false;
}
/**
* 初始化所有 Worker
*/
async initAll() {
if (this.initialized) return;
// 先加载所有适配器
await registry.loadAll();
// 解析登录模式参数:-login 或 -login=workerName
let loginWorkerName = null;
const loginArg = process.argv.find(arg => arg.startsWith('-login'));
const isLoginMode = !!loginArg;
if (loginArg && loginArg.includes('=')) {
loginWorkerName = loginArg.split('=')[1];
logger.info('工作池', `登录模式: 仅初始化 Worker "${loginWorkerName}"`);
} else if (isLoginMode) {
// -login 不带参数:使用第一个 Worker
loginWorkerName = this.config.backend.pool.workers[0]?.name || null;
logger.info('工作池', `登录模式: 仅初始化第一个 Worker "${loginWorkerName}"`);
}
const workerConfigs = this.config.backend.pool.workers;
// 登录模式下只显示初始化 1 个
if (isLoginMode) {
logger.info('工作池', `登录模式: 从 ${workerConfigs.length} 个 Worker 中筛选...`);
} else {
logger.info('工作池', `正在初始化 ${workerConfigs.length} 个 Worker...`);
}
// 过滤并创建 Worker 实例
const validWorkers = [];
for (const workerConfig of workerConfigs) {
// 登录模式过滤:只初始化指定名称的 Worker
if (isLoginMode && workerConfig.name !== loginWorkerName) {
logger.debug('工作池', `[${workerConfig.name}] 跳过 (不匹配登录目标)`);
continue;
}
// 校验 Worker 类型是否有对应的适配器
if (workerConfig.type !== 'merge' && !registry.hasAdapter(workerConfig.type)) {
logger.error('工作池', `Worker [${workerConfig.name}] 的类型 "${workerConfig.type}" 无对应适配器,跳过`);
continue;
}
// Merge 模式:校验所有 mergeTypes
if (workerConfig.type === 'merge') {
const invalidTypes = (workerConfig.mergeTypes || []).filter(t => !registry.hasAdapter(t));
if (invalidTypes.length > 0) {
logger.error('工作池', `Worker [${workerConfig.name}] 的 mergeTypes 包含无效类型: ${invalidTypes.join(', ')}`);
continue;
}
}
validWorkers.push(new Worker(this.config, workerConfig));
}
// 登录模式下如果没有匹配的 Worker
if (isLoginMode && validWorkers.length === 0) {
// 列出可用的 Worker 名称
const availableNames = workerConfigs.map(w => w.name).join(', ');
throw new Error(`登录模式未找到 Worker "${loginWorkerName}"。可用的 Worker: ${availableNames}`);
}
// 按 userDataDir 分组
const browserMap = new Map(); // userDataDir -> { browser, proxyConfig, firstWorkerName }
for (const worker of validWorkers) {
const existing = browserMap.get(worker.userDataDir);
if (existing) {
// 复用已有浏览器 - 检测代理配置冲突
const workerProxy = JSON.stringify(worker.proxyConfig || null);
const existingProxy = JSON.stringify(existing.proxyConfig || null);
if (workerProxy !== existingProxy) {
logger.warn('工作池', `[${worker.name}] 代理配置与 [${existing.firstWorkerName}] 不一致,将使用后者的配置`);
}
logger.debug('工作池', `[${worker.name}] 将与其他 Worker 共享浏览器 (${worker.userDataDir})`);
await worker.init(existing.browser);
} else {
// 启动新浏览器
await worker.init();
browserMap.set(worker.userDataDir, {
browser: worker.browser,
proxyConfig: worker.proxyConfig,
firstWorkerName: worker.name
});
}
this.workers.push(worker);
}
this.initialized = true;
logger.info('工作池', `工作池初始化完成,共 ${this.workers.length} 个 Worker 就绪 (${browserMap.size} 个浏览器实例)`);
}
/**
* 根据模型选择 Worker
* @param {string} modelId - 模型 ID
* @returns {Worker}
*/
selectWorker(modelId) {
// 1. 筛选:找出所有支持该模型的 Worker
const candidates = this.workers.filter(w => w.supports(modelId));
if (candidates.length === 0) {
throw new Error(`没有 Worker 支持模型: ${modelId}`);
}
if (candidates.length === 1) {
return candidates[0];
}
// 2. 决策:根据策略选择
switch (this.strategy) {
case 'round_robin': {
const idx = this.roundRobinIndex % candidates.length;
this.roundRobinIndex++;
return candidates[idx];
}
case 'random': {
const idx = Math.floor(Math.random() * candidates.length);
return candidates[idx];
}
case 'least_busy':
default: {
return candidates.reduce((min, w) => w.busyCount < min.busyCount ? w : min, candidates[0]);
}
}
}
/**
* 分发生图任务
*/
async generateImage(ctx, prompt, paths, modelId, meta) {
const worker = this.selectWorker(modelId);
logger.debug('工作池', `任务分发至: ${worker.name} (busy: ${worker.busyCount})`);
return await worker.generateImage(ctx, prompt, paths, modelId, meta);
}
/**
* 解析模型 ID(用于请求前校验)
* @param {string} modelKey - 模型 key
* @returns {string|null} 返回 workerName|type|realId 格式,或 null
*/
resolveModelId(modelKey) {
for (const worker of this.workers) {
const resolved = worker.resolveModelId(modelKey);
if (resolved) {
return `${worker.name}|${resolved.type}|${resolved.realId}`;
}
}
return null;
}
/**
* 获取所有模型列表(聚合去重)
* @returns {object}
*/
getModels() {
const allModels = [];
const seenIds = new Set();
for (const worker of this.workers) {
const models = worker.getModels();
for (const m of models) {
if (!seenIds.has(m.id)) {
seenIds.add(m.id);
allModels.push(m);
}
}
}
return { object: 'list', data: allModels };
}
/**
* 获取图片策略
* @param {string} modelKey - 模型 key
* @returns {string}
*/
getImagePolicy(modelKey) {
for (const worker of this.workers) {
if (worker.supports(modelKey)) {
return worker.getImagePolicy(modelKey);
}
}
return 'optional';
}
/**
* 获取指定实例的 Cookies
* @param {string} [instanceName] - 实例名称,不提供则返回第一个
* @param {string} [domain] - 指定域名
* @returns {Promise<{instance: string, cookies: object[]}>}
*/
async getCookies(instanceName, domain) {
let worker;
if (instanceName) {
worker = this.workers.find(w => w.instanceName === instanceName);
if (!worker) {
throw new Error(`浏览器实例不存在: ${instanceName}`);
}
} else {
worker = this.workers[0];
if (!worker) {
throw new Error('工作池中没有可用的 Worker');
}
}
const cookies = await worker.getCookies(domain);
return { instance: worker.instanceName, cookies };
}
/**
* 触发所有 merge Worker 的监控导航
*/
async navigateToMonitor() {
for (const worker of this.workers) {
if (worker.type === 'merge' && worker.busyCount === 0) {
await worker.navigateToMonitor();
}
}
}
/**
* 获取第一个 Worker 的 page(兼容旧接口)
* @returns {object|null}
*/
getFirstPage() {
return this.workers[0]?.page || null;
}
}
export { Worker, PoolManager };
+274
View File
@@ -0,0 +1,274 @@
/**
* @fileoverview 适配器注册表
* @description 自动扫描 adapter/ 目录,加载所有适配器的 manifest,提供统一查询接口。
*
* 设计目标:
* - 新增适配器只需在 adapter/ 目录添加文件,无需修改框架代码
* - 提供模型查询、策略查询、导航处理器聚合等统一接口
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { logger } from '../utils/logger.js';
// 获取当前目录
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ADAPTER_DIR = path.join(__dirname, 'adapter');
/**
* 图片输入策略枚举
*/
export const IMAGE_POLICY = {
OPTIONAL: 'optional',
REQUIRED: 'required',
FORBIDDEN: 'forbidden'
};
/**
* 适配器注册表类
*/
class AdapterRegistry {
constructor() {
/** @type {Map<string, object>} */
this.adapters = new Map();
this.loaded = false;
}
/**
* 扫描并加载所有适配器
*/
async loadAll() {
if (this.loaded) return;
//logger.info('注册表', `正在扫描适配器目录: ${ADAPTER_DIR}`);
logger.info('注册表', `正在扫描适配器目录...`);
const files = fs.readdirSync(ADAPTER_DIR).filter(f => f.endsWith('.js'));
for (const file of files) {
const filePath = path.join(ADAPTER_DIR, file);
try {
const module = await import(`file://${filePath}`);
if (!module.manifest) {
logger.warn('注册表', `跳过 ${file}: 未导出 manifest`);
continue;
}
const manifest = module.manifest;
// 校验必需字段
if (!this.validateManifest(manifest, file)) {
continue;
}
this.adapters.set(manifest.id, manifest);
logger.debug('注册表', `已加载适配器: ${manifest.id} (${manifest.displayName || file})`);
} catch (err) {
logger.error('注册表', `加载 ${file} 失败: ${err.message}`);
}
}
this.loaded = true;
logger.info('注册表', `适配器加载完成,共 ${this.adapters.size} 个可用`);
}
/**
* 校验 manifest 必需字段
* @param {object} manifest
* @param {string} fileName
* @returns {boolean}
*/
validateManifest(manifest, fileName) {
const errors = [];
if (!manifest.id || typeof manifest.id !== 'string') {
errors.push('缺少 id 或类型不正确');
}
if (!manifest.generateImage || typeof manifest.generateImage !== 'function') {
errors.push('缺少 generateImage 函数');
}
if (!manifest.models || !Array.isArray(manifest.models)) {
errors.push('缺少 models 数组');
} else {
for (let i = 0; i < manifest.models.length; i++) {
const m = manifest.models[i];
if (!m.id) {
errors.push(`models[${i}] 缺少 id`);
}
if (!m.imagePolicy || !Object.values(IMAGE_POLICY).includes(m.imagePolicy)) {
errors.push(`models[${i}] imagePolicy 无效`);
}
}
}
if (errors.length > 0) {
logger.error('注册表', `${fileName} manifest 校验失败: ${errors.join('; ')}`);
return false;
}
return true;
}
/**
* 获取适配器
* @param {string} id - 适配器 ID
* @returns {object|null}
*/
getAdapter(id) {
return this.adapters.get(id) || null;
}
/**
* 获取所有已注册的适配器 ID
* @returns {string[]}
*/
getAdapterIds() {
return Array.from(this.adapters.keys());
}
/**
* 检查适配器是否存在
* @param {string} id
* @returns {boolean}
*/
hasAdapter(id) {
return this.adapters.has(id);
}
/**
* 获取适配器的目标 URL
* @param {string} id - 适配器 ID
* @param {object} config - 全局配置
* @param {object} workerConfig - Worker 配置
* @returns {string}
*/
getTargetUrl(id, config, workerConfig) {
const adapter = this.getAdapter(id);
if (!adapter) return 'about:blank';
if (typeof adapter.getTargetUrl === 'function') {
return adapter.getTargetUrl(config, workerConfig) || 'about:blank';
}
return adapter.targetUrl || 'about:blank';
}
/**
* 获取适配器的导航处理器
* @param {string} id - 适配器 ID
* @returns {Function[]}
*/
getNavigationHandlers(id) {
const adapter = this.getAdapter(id);
if (!adapter) return [];
return adapter.navigationHandlers || [];
}
/**
* 获取适配器的输入框就绪校验函数
* @param {string} id - 适配器 ID
* @returns {Function|null}
*/
getWaitInput(id) {
const adapter = this.getAdapter(id);
if (!adapter) return null;
return adapter.waitInput || null;
}
/**
* 获取指定适配器的模型列表 (OpenAI 格式)
* @param {string} id - 适配器 ID
* @returns {object}
*/
getModelsForAdapter(id) {
const adapter = this.getAdapter(id);
if (!adapter || !adapter.models) {
return { object: 'list', data: [] };
}
const data = adapter.models.map(m => ({
id: m.id,
object: 'model',
created: Math.floor(Date.now() / 1000),
owned_by: id,
image_policy: m.imagePolicy
}));
return { object: 'list', data };
}
/**
* 解析模型 ID
* @param {string} adapterId - 适配器 ID
* @param {string} modelKey - 模型 key
* @returns {string|null} 内部 ID,或 null
*/
resolveModelId(adapterId, modelKey) {
const adapter = this.getAdapter(adapterId);
if (!adapter) return null;
// 如果适配器提供了自定义解析函数
if (typeof adapter.resolveModelId === 'function') {
return adapter.resolveModelId(modelKey);
}
// 默认行为:检查 modelKey 是否在 models 中
const model = adapter.models.find(m => m.id === modelKey);
if (model) {
return model.codeName || model.id;
}
return null;
}
/**
* 获取模型的图片策略
* @param {string} adapterId - 适配器 ID
* @param {string} modelKey - 模型 key
* @returns {string}
*/
getImagePolicy(adapterId, modelKey) {
const adapter = this.getAdapter(adapterId);
if (!adapter || !adapter.models) {
return IMAGE_POLICY.OPTIONAL;
}
const model = adapter.models.find(m => m.id === modelKey);
return model?.imagePolicy || IMAGE_POLICY.OPTIONAL;
}
/**
* 聚合所有适配器的模型列表
* @returns {object}
*/
getAllModels() {
const allModels = [];
for (const [id, adapter] of this.adapters) {
if (adapter.models) {
for (const m of adapter.models) {
allModels.push({
id: m.id,
object: 'model',
created: Math.floor(Date.now() / 1000),
owned_by: id,
image_policy: m.imagePolicy
});
}
}
}
return { object: 'list', data: allModels };
}
}
// 导出单例
const registry = new AdapterRegistry();
export { AdapterRegistry, registry };
+13 -7
View File
@@ -199,19 +199,26 @@ export function normalizeHttpError(response, content = null) {
* 根据 camoufoxFingerprints.json 动态生成请求头,保持与浏览器指纹一致
*
* @param {string} url - 图片 URL
* @param {object} config - 配置对象(需包含 proxy 配置)
* @param {object} options - 下载选项
* @param {object} [options.proxyConfig] - Worker 级代理配置
* @param {string} [options.userDataDir] - 用户数据目录(用于读取对应的指纹文件)
* @returns {Promise<{ image?: string, error?: string }>} 下载结果
*/
export async function downloadImage(url, config) {
export async function downloadImage(url, options = {}) {
// 动态导入依赖
const { gotScraping } = await import('got-scraping');
const fs = await import('fs');
const path = await import('path');
const { getProxyConfig, getHttpProxy } = await import('../utils/proxy.js');
const { getHttpProxy } = await import('../utils/proxy.js');
const { proxyConfig = null, userDataDir } = options;
try {
// 读取指纹文件获取浏览器信息
const fingerprintPath = path.join(process.cwd(), 'data', 'camoufoxFingerprints.json');
// 读取指纹文件获取浏览器信息(优先使用 userDataDir 内的指纹)
let fingerprintPath = userDataDir
? path.join(userDataDir, 'fingerprint.json')
: path.join(process.cwd(), 'data', 'camoufoxUserData', 'fingerprint.json');
let browserName = 'firefox';
let browserMinVersion = 100;
let os = 'windows';
@@ -242,8 +249,7 @@ export async function downloadImage(url, config) {
}
}
// 获取代理配置
const proxyConfig = getProxyConfig(config);
// 获取代理配置(直接使用传入的 proxyConfig
const proxyUrl = await getHttpProxy(proxyConfig);
const options = {