mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 增加多网站聚合模式
This commit is contained in:
@@ -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
@@ -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:
|
||||
# 入口链接
|
||||
|
||||
@@ -348,5 +348,5 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export { initBrowser, generateImage };
|
||||
export { initBrowser, generateImage, handleAccountChooser };
|
||||
|
||||
|
||||
@@ -413,4 +413,4 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export { initBrowser, generateImage };
|
||||
export { initBrowser, generateImage, handleDiscordAuth };
|
||||
|
||||
+178
-30
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.`;
|
||||
|
||||
Reference in New Issue
Block a user