diff --git a/CHANGELOG.md b/CHANGELOG.md index f020ded..c75c002 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,11 +15,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 添加下载 GeoLite2-City.mmdb 数据库的步骤 - **增加服务器自检** - 自动排查缺少的依赖和未应用的补丁并提供解决方案,降低使用门槛 +- **聚合模式监控** + - 在聚合模式下闲时跳转需要监控的网站,确保登录过期自动续登保持Cookie为最新 ### Changed - **重构部分** - 重构服务器部分,并整理项目目录 - 加强注释,便于后期维护 + - 支持获取指定域名的 Cookie ## [2.3.0] - 2025-12-12 diff --git a/README.md b/README.md index 1f5f997..6aac8f8 100644 --- a/README.md +++ b/README.md @@ -230,8 +230,9 @@ curl -X GET http://127.0.0.1:3000/v1/models \ **功能说明**:可利用本项目的自动续登功能获取最新Cookie给其他工具使用。 **请求端点** +支持使用`domain`参数获取指定域名的Cookie ``` -GET http://127.0.0.1:3000/v1/cookies +GET http://127.0.0.1:3000/v1/cookies (?domain=lmarena.ai) ```
diff --git a/config.example.yaml b/config.example.yaml index 399d668..5fc2a4c 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -30,6 +30,8 @@ backend: type: - zai_is - lmarena + # 挂机监控 + monitor: zai_is # Gemini Business 设置 geminiBiz: diff --git a/server.js b/server.js index 52638a8..28c06ba 100644 --- a/server.js +++ b/server.js @@ -93,7 +93,10 @@ const queueManager = createQueueManager( { initBrowser, generateImage, - config + config, + navigateToMonitor: backend.navigateToMonitor + ? () => backend.navigateToMonitor(config) + : null } ); diff --git a/src/backend/index.js b/src/backend/index.js index 9f34d3d..416f277 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -48,8 +48,10 @@ 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 }; } @@ -100,14 +102,14 @@ const MergedBackend = { const subContext = { ...ctx, page: MergedBackend._globalPage, - config: cfg + config: ctx.config }; return adapter.generateImage(subContext, prompt, paths, realId, meta); }, resolveModelId: (modelKey) => { - const types = config.backend.merge.type; + const types = MergedBackend._config.backend.merge.type; // 支持 backend/model 格式指定后端 (如 lmarena/seedream-4.5) if (modelKey.includes('/')) { @@ -128,7 +130,7 @@ const MergedBackend = { }, getModels: () => { - const types = config.backend.merge.type; + const types = MergedBackend._config.backend.merge.type; const allModels = []; const seenIds = new Set(); @@ -167,7 +169,7 @@ const MergedBackend = { }, getImagePolicy: (modelKey) => { - const types = config.backend.merge.type; + const types = MergedBackend._config.backend.merge.type; // 支持 backend/model 格式 if (modelKey.includes('/')) { @@ -184,6 +186,49 @@ const MergedBackend = { if (realId) return modelsModule.getImagePolicy(type, modelKey); } return 'optional'; + }, + + /** + * 空闲时导航到监控页面(用于自动续签 Cookie) + * @param {object} cfg - 配置对象 + * @returns {Promise} + */ + 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}`); + } } }; diff --git a/src/server/http/routes.js b/src/server/http/routes.js index 6906450..47cbb4a 100644 --- a/src/server/http/routes.js +++ b/src/server/http/routes.js @@ -58,8 +58,9 @@ export function createRouter(context) { * 处理 GET /v1/cookies * @param {import('http').ServerResponse} res - HTTP 响应 * @param {string} requestId - 请求 ID + * @param {string} [domain] - 可选,指定获取某个域名的 Cookies */ - async function handleCookies(res, requestId) { + async function handleCookies(res, requestId, domain) { const browserContext = queueManager.getBrowserContext(); if (!browserContext?.page) { @@ -69,7 +70,16 @@ export function createRouter(context) { try { 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 }); } catch (err) { 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); - } else if (req.method === 'GET' && req.url === '/v1/cookies') { - await handleCookies(res, requestId); - } else if (req.method === 'POST' && req.url?.startsWith('/v1/chat/completions')) { + } else if (req.method === 'GET' && pathname === '/v1/cookies') { + const domain = parsedUrl.searchParams.get('domain'); + await handleCookies(res, requestId, domain); + } else if (req.method === 'POST' && pathname.startsWith('/v1/chat/completions')) { await handleChatCompletions(req, res, requestId); } else { res.writeHead(404); diff --git a/src/server/queue.js b/src/server/queue.js index 805c8de..2d1e964 100644 --- a/src/server/queue.js +++ b/src/server/queue.js @@ -51,7 +51,7 @@ import { */ export function createQueueManager(queueConfig, callbacks) { const { maxConcurrent, maxQueueSize, keepaliveMode } = queueConfig; - const { initBrowser, generateImage, config } = callbacks; + const { initBrowser, generateImage, config, navigateToMonitor } = callbacks; /** @type {TaskContext[]} */ const queue = []; @@ -149,7 +149,13 @@ export function createQueueManager(queueConfig, callbacks) { */ 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(); @@ -201,6 +207,10 @@ export function createQueueManager(queueConfig, callbacks) { */ async function initializeBrowser() { browserContext = await initBrowser(config); + // 初始化完成后,触发首次监控跳转 + if (navigateToMonitor) { + navigateToMonitor().catch(() => { }); + } return browserContext; } diff --git a/src/utils/config.js b/src/utils/config.js index a20daad..cf848dd 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -94,7 +94,8 @@ export function loadConfig() { if (!config.backend.merge) { config.backend.merge = { enable: false, - type: ['zai_is', 'lmarena'] + type: ['zai_is', 'lmarena'], + monitor: null }; }