feat: 增加多网站聚合模式

This commit is contained in:
foxhui
2025-12-12 17:58:46 +08:00
Unverified
parent 589cd68040
commit c6ffc1af2f
7 changed files with 227 additions and 38 deletions
+6
View File
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.2.3] - 2025-12-12
### Added
- **后端聚合**
- 可根据请求的模型id自动选择可用的适配器且遵循配置文件中的优先级
## [2.2.2] - 2025-12-12
### Added
+10 -1
View File
@@ -25,7 +25,16 @@ backend:
# - nanobananafree_ai (Nano Banana Free)
# - zai_is (zAI)
type: lmarena
# 聚合模式设置 (优先级高于 type)
# 启用后,系统将复用单个浏览器标签页处理所有后端任务
merge:
enable: false
# 聚合列表 (按优先级排序,模型匹配时优先使用靠前的适配器)
type:
- zai_is
- lmarena
# Gemini Business 设置
geminiBiz:
# 入口链接
+1 -1
View File
@@ -348,5 +348,5 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
}
}
export { initBrowser, generateImage };
export { initBrowser, generateImage, handleAccountChooser };
+1 -1
View File
@@ -413,4 +413,4 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
}
}
export { initBrowser, generateImage };
export { initBrowser, generateImage, handleDiscordAuth };
+178 -30
View File
@@ -1,6 +1,11 @@
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 { logger } from '../utils/logger.js';
// 导入适配器
import * as lmarenaBackend from './adapter/lmarena.js';
import * as geminiBackend from './adapter/gemini_biz.js';
import * as nanobananafreeBackend from './adapter/nanobananafree_ai.js';
@@ -26,40 +31,183 @@ config.paths = {
tempDir: TEMP_DIR
};
let activeBackend;
// 适配器映射表
const ADAPTER_MAP = {
'gemini_biz': geminiBackend,
'nanobananafree_ai': nanobananafreeBackend,
'zai_is': zaiIsBackend,
'lmarena': lmarenaBackend
};
switch (config.backend?.type) {
case 'gemini_biz':
activeBackend = {
name: 'gemini_biz',
initBrowser: (cfg) => geminiBackend.initBrowser(cfg),
generateImage: (ctx, prompt, paths, model, meta) => geminiBackend.generateImage(ctx, prompt, paths, model, meta)
// 1. 单一后端模式实现
const SingleBackend = {
name: config.backend.type,
initBrowser: async (cfg) => {
const adapter = ADAPTER_MAP[cfg.backend.type] || lmarenaBackend;
return adapter.initBrowser(cfg);
},
generateImage: async (ctx, prompt, paths, modelId, meta) => {
const adapter = ADAPTER_MAP[config.backend.type] || lmarenaBackend;
return adapter.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)
};
// 2. 聚合后端模式实现
const MergedBackend = {
name: 'aggregated',
_globalBrowser: null,
_globalPage: null,
initBrowser: async (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) { }
}
};
break;
case 'nanobananafree_ai':
activeBackend = {
name: 'nanobananafree_ai',
initBrowser: (cfg) => nanobananafreeBackend.initBrowser(cfg),
generateImage: (ctx, prompt, paths, model, meta) => nanobananafreeBackend.generateImage(ctx, prompt, paths, model, meta)
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: config
};
break;
case 'zai_is':
activeBackend = {
name: 'zai_is',
initBrowser: (cfg) => zaiIsBackend.initBrowser(cfg),
generateImage: (ctx, prompt, paths, model, meta) => zaiIsBackend.generateImage(ctx, prompt, paths, model, meta)
};
break;
case 'lmarena':
default:
activeBackend = {
name: 'lmarena',
initBrowser: (cfg) => lmarenaBackend.initBrowser(cfg),
generateImage: (ctx, prompt, paths, model, meta) => lmarenaBackend.generateImage(ctx, prompt, paths, model, meta)
};
break;
}
return adapter.generateImage(subContext, prompt, paths, realId, meta);
},
resolveModelId: (modelKey) => {
const types = 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 = 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 = 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';
}
};
export function getBackend() {
const isMerge = config.backend.merge && config.backend.merge.enable;
const activeBackend = isMerge ? MergedBackend : SingleBackend;
logger.info('适配器', `后端模式: ${isMerge ? '聚合' : '独立'}`);
return { config, TEMP_DIR, ...activeBackend };
}
+17
View File
@@ -51,6 +51,15 @@ backend:
# - nanobananafree_ai (Nano Banana Free)
# - zai_is (zAI)
type: lmarena
# 聚合模式设置 (优先级高于 type)
# 启用后,系统将复用单个浏览器标签页处理所有后端任务
merge:
enable: false
# 聚合列表 (按优先级排序,模型匹配时优先使用靠前的适配器)
type:
- zai_is
- lmarena
# Gemini Business 设置
geminiBiz:
@@ -167,6 +176,14 @@ export function loadConfig() {
};
}
// 新增:Merge 配置初始化
if (!config.backend.merge) {
config.backend.merge = {
enable: false,
type: ['zai_is', 'lmarena']
};
}
// 校验 GeminiBiz 配置
if (config.backend.type === 'gemini_biz') {
if (!config.backend.geminiBiz || !config.backend.geminiBiz.entryUrl) {
+14 -5
View File
@@ -3,7 +3,7 @@ import fs from 'fs';
import path from 'path';
import sharp from 'sharp';
import { getBackend } from './lib/backend/index.js';
import { getModelsForBackend, resolveModelId, getImagePolicy, IMAGE_POLICY } from './lib/backend/models.js';
import { IMAGE_POLICY } from './lib/backend/models.js';
import { logger } from './lib/utils/logger.js';
import crypto from 'crypto';
import { spawn, spawnSync } from 'child_process';
@@ -216,7 +216,16 @@ if (displayResult === 'XVFB_REDIRECT') {
// ==================== 服务器主逻辑 ====================
// 使用统一后端获取配置和函数
const { config, name, initBrowser, generateImage, TEMP_DIR } = getBackend();
const {
config,
name,
initBrowser,
generateImage,
TEMP_DIR,
resolveModelId,
getModels,
getImagePolicy
} = getBackend();
const PORT = config.server.port || 3000;
const AUTH_TOKEN = config.server.auth;
@@ -408,7 +417,7 @@ async function startServer() {
// --- 路由分发 ---
// 1. 模型列表接口
if (req.method === 'GET' && req.url === '/v1/models') {
const models = getModelsForBackend(name);
const models = getModels();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(models));
return;
@@ -561,7 +570,7 @@ async function startServer() {
// 解析模型参数
let modelId = null;
if (data.model) {
modelId = resolveModelId(name, data.model);
modelId = resolveModelId(data.model);
if (modelId) {
logger.info('服务器', `触发模型: ${data.model} (${modelId})`, { id });
} else {
@@ -583,7 +592,7 @@ async function startServer() {
// 图片策略校验
const hasImage = imagePaths.length > 0;
const policy = data.model ? getImagePolicy(name, data.model) : IMAGE_POLICY.OPTIONAL;
const policy = data.model ? getImagePolicy(data.model) : IMAGE_POLICY.OPTIONAL;
if (policy === IMAGE_POLICY.REQUIRED && !hasImage) {
const errorMsg = `Model ${data.model} requires a reference image.`;