mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 聚合模式增加闲时监控,支持获取指定域名的Cookie
This commit is contained in:
@@ -15,11 +15,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- 添加下载 GeoLite2-City.mmdb 数据库的步骤
|
- 添加下载 GeoLite2-City.mmdb 数据库的步骤
|
||||||
- **增加服务器自检**
|
- **增加服务器自检**
|
||||||
- 自动排查缺少的依赖和未应用的补丁并提供解决方案,降低使用门槛
|
- 自动排查缺少的依赖和未应用的补丁并提供解决方案,降低使用门槛
|
||||||
|
- **聚合模式监控**
|
||||||
|
- 在聚合模式下闲时跳转需要监控的网站,确保登录过期自动续登保持Cookie为最新
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- **重构部分**
|
- **重构部分**
|
||||||
- 重构服务器部分,并整理项目目录
|
- 重构服务器部分,并整理项目目录
|
||||||
- 加强注释,便于后期维护
|
- 加强注释,便于后期维护
|
||||||
|
- 支持获取指定域名的 Cookie
|
||||||
|
|
||||||
## [2.3.0] - 2025-12-12
|
## [2.3.0] - 2025-12-12
|
||||||
|
|
||||||
|
|||||||
@@ -230,8 +230,9 @@ curl -X GET http://127.0.0.1:3000/v1/models \
|
|||||||
**功能说明**:可利用本项目的自动续登功能获取最新Cookie给其他工具使用。
|
**功能说明**:可利用本项目的自动续登功能获取最新Cookie给其他工具使用。
|
||||||
|
|
||||||
**请求端点**
|
**请求端点**
|
||||||
|
支持使用`domain`参数获取指定域名的Cookie
|
||||||
```
|
```
|
||||||
GET http://127.0.0.1:3000/v1/cookies
|
GET http://127.0.0.1:3000/v1/cookies (?domain=lmarena.ai)
|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ backend:
|
|||||||
type:
|
type:
|
||||||
- zai_is
|
- zai_is
|
||||||
- lmarena
|
- lmarena
|
||||||
|
# 挂机监控
|
||||||
|
monitor: zai_is
|
||||||
|
|
||||||
# Gemini Business 设置
|
# Gemini Business 设置
|
||||||
geminiBiz:
|
geminiBiz:
|
||||||
|
|||||||
@@ -93,7 +93,10 @@ const queueManager = createQueueManager(
|
|||||||
{
|
{
|
||||||
initBrowser,
|
initBrowser,
|
||||||
generateImage,
|
generateImage,
|
||||||
config
|
config,
|
||||||
|
navigateToMonitor: backend.navigateToMonitor
|
||||||
|
? () => backend.navigateToMonitor(config)
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+49
-4
@@ -48,8 +48,10 @@ const MergedBackend = {
|
|||||||
name: 'merge',
|
name: 'merge',
|
||||||
_globalBrowser: null,
|
_globalBrowser: null,
|
||||||
_globalPage: null,
|
_globalPage: null,
|
||||||
|
_config: null, // 保存配置引用
|
||||||
|
|
||||||
initBrowser: async (cfg) => {
|
initBrowser: async (cfg) => {
|
||||||
|
MergedBackend._config = cfg; // 保存配置
|
||||||
if (MergedBackend._globalPage && !MergedBackend._globalPage.isClosed()) {
|
if (MergedBackend._globalPage && !MergedBackend._globalPage.isClosed()) {
|
||||||
return { browser: MergedBackend._globalBrowser, page: MergedBackend._globalPage, config: cfg };
|
return { browser: MergedBackend._globalBrowser, page: MergedBackend._globalPage, config: cfg };
|
||||||
}
|
}
|
||||||
@@ -100,14 +102,14 @@ const MergedBackend = {
|
|||||||
const subContext = {
|
const subContext = {
|
||||||
...ctx,
|
...ctx,
|
||||||
page: MergedBackend._globalPage,
|
page: MergedBackend._globalPage,
|
||||||
config: cfg
|
config: ctx.config
|
||||||
};
|
};
|
||||||
|
|
||||||
return adapter.generateImage(subContext, prompt, paths, realId, meta);
|
return adapter.generateImage(subContext, prompt, paths, realId, meta);
|
||||||
},
|
},
|
||||||
|
|
||||||
resolveModelId: (modelKey) => {
|
resolveModelId: (modelKey) => {
|
||||||
const types = config.backend.merge.type;
|
const types = MergedBackend._config.backend.merge.type;
|
||||||
|
|
||||||
// 支持 backend/model 格式指定后端 (如 lmarena/seedream-4.5)
|
// 支持 backend/model 格式指定后端 (如 lmarena/seedream-4.5)
|
||||||
if (modelKey.includes('/')) {
|
if (modelKey.includes('/')) {
|
||||||
@@ -128,7 +130,7 @@ const MergedBackend = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getModels: () => {
|
getModels: () => {
|
||||||
const types = config.backend.merge.type;
|
const types = MergedBackend._config.backend.merge.type;
|
||||||
const allModels = [];
|
const allModels = [];
|
||||||
const seenIds = new Set();
|
const seenIds = new Set();
|
||||||
|
|
||||||
@@ -167,7 +169,7 @@ const MergedBackend = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getImagePolicy: (modelKey) => {
|
getImagePolicy: (modelKey) => {
|
||||||
const types = config.backend.merge.type;
|
const types = MergedBackend._config.backend.merge.type;
|
||||||
|
|
||||||
// 支持 backend/model 格式
|
// 支持 backend/model 格式
|
||||||
if (modelKey.includes('/')) {
|
if (modelKey.includes('/')) {
|
||||||
@@ -184,6 +186,49 @@ const MergedBackend = {
|
|||||||
if (realId) return modelsModule.getImagePolicy(type, modelKey);
|
if (realId) return modelsModule.getImagePolicy(type, modelKey);
|
||||||
}
|
}
|
||||||
return 'optional';
|
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}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,9 @@ export function createRouter(context) {
|
|||||||
* 处理 GET /v1/cookies
|
* 处理 GET /v1/cookies
|
||||||
* @param {import('http').ServerResponse} res - HTTP 响应
|
* @param {import('http').ServerResponse} res - HTTP 响应
|
||||||
* @param {string} requestId - 请求 ID
|
* @param {string} requestId - 请求 ID
|
||||||
|
* @param {string} [domain] - 可选,指定获取某个域名的 Cookies
|
||||||
*/
|
*/
|
||||||
async function handleCookies(res, requestId) {
|
async function handleCookies(res, requestId, domain) {
|
||||||
const browserContext = queueManager.getBrowserContext();
|
const browserContext = queueManager.getBrowserContext();
|
||||||
|
|
||||||
if (!browserContext?.page) {
|
if (!browserContext?.page) {
|
||||||
@@ -69,7 +70,16 @@ export function createRouter(context) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const context = browserContext.page.context();
|
const context = browserContext.page.context();
|
||||||
const cookies = await context.cookies();
|
let cookies;
|
||||||
|
|
||||||
|
if (domain) {
|
||||||
|
// 指定域名时,只获取该域名的 Cookies
|
||||||
|
cookies = await context.cookies(domain.startsWith('http') ? domain : `https://${domain}`);
|
||||||
|
} else {
|
||||||
|
// 默认获取所有 Cookies
|
||||||
|
cookies = await context.cookies();
|
||||||
|
}
|
||||||
|
|
||||||
sendJson(res, 200, { cookies });
|
sendJson(res, 200, { cookies });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('服务器', '获取 Cookies 失败', { id: requestId, error: err.message });
|
logger.error('服务器', '获取 Cookies 失败', { id: requestId, error: err.message });
|
||||||
@@ -179,11 +189,15 @@ export function createRouter(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 路由分发
|
// 路由分发
|
||||||
if (req.method === 'GET' && req.url === '/v1/models') {
|
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
|
||||||
|
const pathname = parsedUrl.pathname;
|
||||||
|
|
||||||
|
if (req.method === 'GET' && pathname === '/v1/models') {
|
||||||
handleModels(res);
|
handleModels(res);
|
||||||
} else if (req.method === 'GET' && req.url === '/v1/cookies') {
|
} else if (req.method === 'GET' && pathname === '/v1/cookies') {
|
||||||
await handleCookies(res, requestId);
|
const domain = parsedUrl.searchParams.get('domain');
|
||||||
} else if (req.method === 'POST' && req.url?.startsWith('/v1/chat/completions')) {
|
await handleCookies(res, requestId, domain);
|
||||||
|
} else if (req.method === 'POST' && pathname.startsWith('/v1/chat/completions')) {
|
||||||
await handleChatCompletions(req, res, requestId);
|
await handleChatCompletions(req, res, requestId);
|
||||||
} else {
|
} else {
|
||||||
res.writeHead(404);
|
res.writeHead(404);
|
||||||
|
|||||||
+12
-2
@@ -51,7 +51,7 @@ import {
|
|||||||
*/
|
*/
|
||||||
export function createQueueManager(queueConfig, callbacks) {
|
export function createQueueManager(queueConfig, callbacks) {
|
||||||
const { maxConcurrent, maxQueueSize, keepaliveMode } = queueConfig;
|
const { maxConcurrent, maxQueueSize, keepaliveMode } = queueConfig;
|
||||||
const { initBrowser, generateImage, config } = callbacks;
|
const { initBrowser, generateImage, config, navigateToMonitor } = callbacks;
|
||||||
|
|
||||||
/** @type {TaskContext[]} */
|
/** @type {TaskContext[]} */
|
||||||
const queue = [];
|
const queue = [];
|
||||||
@@ -149,7 +149,13 @@ export function createQueueManager(queueConfig, callbacks) {
|
|||||||
*/
|
*/
|
||||||
async function processQueue() {
|
async function processQueue() {
|
||||||
// 如果正在处理的任务已满,或队列为空,则停止
|
// 如果正在处理的任务已满,或队列为空,则停止
|
||||||
if (processingCount >= maxConcurrent || queue.length === 0) return;
|
if (processingCount >= maxConcurrent || queue.length === 0) {
|
||||||
|
// 队列空闲时,触发监控跳转
|
||||||
|
if (processingCount === 0 && queue.length === 0 && navigateToMonitor) {
|
||||||
|
navigateToMonitor().catch(() => { });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 取出下一个任务
|
// 取出下一个任务
|
||||||
const task = queue.shift();
|
const task = queue.shift();
|
||||||
@@ -201,6 +207,10 @@ export function createQueueManager(queueConfig, callbacks) {
|
|||||||
*/
|
*/
|
||||||
async function initializeBrowser() {
|
async function initializeBrowser() {
|
||||||
browserContext = await initBrowser(config);
|
browserContext = await initBrowser(config);
|
||||||
|
// 初始化完成后,触发首次监控跳转
|
||||||
|
if (navigateToMonitor) {
|
||||||
|
navigateToMonitor().catch(() => { });
|
||||||
|
}
|
||||||
return browserContext;
|
return browserContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -94,7 +94,8 @@ export function loadConfig() {
|
|||||||
if (!config.backend.merge) {
|
if (!config.backend.merge) {
|
||||||
config.backend.merge = {
|
config.backend.merge = {
|
||||||
enable: false,
|
enable: false,
|
||||||
type: ['zai_is', 'lmarena']
|
type: ['zai_is', 'lmarena'],
|
||||||
|
monitor: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user