From 32a29e61e9d987a3075f8d442730c7f9b501b19f Mon Sep 17 00:00:00 2001 From: yuyx <237899745@qq.com> Date: Wed, 10 Dec 2025 20:31:49 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E4=BC=98=E5=8C=96=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E6=B1=A0=E5=92=8C=E5=B9=B6=E5=8F=91=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 浏览器池改为按需启动模式 - 启动时不创建浏览器,有截图任务时才启动 - 空闲5分钟后自动关闭浏览器释放资源 2. 修复截图并发数保存问题 - 修复database.py中缺少保存max_screenshot_concurrent的代码 3. 去掉并发数上限限制 - 管理员可自由设置并发数,不再限制1-20/1-5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app.py | 8 ++++---- browser_pool_worker.py | 41 ++++++++++++++++++++++------------------- database.py | 4 ++++ templates/admin.html | 26 +++++++++++++------------- 4 files changed, 43 insertions(+), 36 deletions(-) diff --git a/app.py b/app.py index d6e415d..42c4308 100755 --- a/app.py +++ b/app.py @@ -3444,15 +3444,15 @@ if __name__ == '__main__': print("默认管理员: admin/admin") print("=" * 60 + "\n") - # 同步初始化浏览器池(必须在socketio.run之前,否则eventlet会导致asyncio冲突) + # 初始化浏览器工作线程池(按需模式,启动时不创建浏览器) try: system_cfg = database.get_system_config() pool_size = system_cfg.get('max_screenshot_concurrent', 3) if system_cfg else 3 - print(f"正在预热 {pool_size} 个浏览器实例(截图加速)...") + print(f"初始化截图线程池({pool_size}个worker,按需启动浏览器,空闲5分钟后自动关闭)...") init_browser_worker_pool(pool_size=pool_size) - print("✓ 浏览器池初始化完成") + print("✓ 截图线程池初始化完成") except Exception as e: - print(f"警告: 浏览器池初始化失败: {e}") + print(f"警告: 截图线程池初始化失败: {e}") socketio.run(app, host=config.SERVER_HOST, port=config.SERVER_PORT, debug=config.DEBUG, allow_unsafe_werkzeug=True) diff --git a/browser_pool_worker.py b/browser_pool_worker.py index 18dfcc6..88cc95b 100755 --- a/browser_pool_worker.py +++ b/browser_pool_worker.py @@ -98,26 +98,33 @@ class BrowserWorker(threading.Thread): return self._create_browser() def run(self): - """工作线程主循环""" - self.log("Worker启动") - - # 初始创建浏览器 - if not self._create_browser(): - self.log("初始浏览器创建失败,Worker退出") - return + """工作线程主循环 - 按需启动浏览器模式""" + self.log("Worker启动(按需模式,等待任务时不占用浏览器资源)") + last_task_time = 0 + IDLE_TIMEOUT = 300 # 空闲5分钟后关闭浏览器 while self.running: try: - # 从队列获取任务(带超时,以便能响应停止信号) + # 从队列获取任务(带超时,以便能响应停止信号和空闲检查) self.idle = True - task = self.task_queue.get(timeout=1) + try: + task = self.task_queue.get(timeout=10) + except queue.Empty: + # 检查是否需要关闭空闲的浏览器 + if self.browser_instance and last_task_time > 0: + idle_time = time.time() - last_task_time + if idle_time > IDLE_TIMEOUT: + self.log(f"空闲{int(idle_time)}秒,关闭浏览器释放资源") + self._close_browser() + continue + self.idle = False if task is None: # None作为停止信号 self.log("收到停止信号") break - # 确保浏览器可用 + # 按需创建或确保浏览器可用 if not self._ensure_browser(): self.log("浏览器不可用,任务失败") task['callback'](None, "浏览器不可用") @@ -140,20 +147,19 @@ class BrowserWorker(threading.Thread): result = task_func(self.browser_instance, *task_args, **task_kwargs) callback(result, None) self.log(f"任务执行成功") + last_task_time = time.time() except Exception as e: self.log(f"任务执行失败: {e}") callback(None, str(e)) self.failed_tasks += 1 + last_task_time = time.time() # 任务失败后,检查浏览器健康 if not self._check_browser_health(): self.log("任务失败导致浏览器异常,将在下次任务前重建") self._close_browser() - except queue.Empty: - # 队列为空,继续等待 - continue except Exception as e: self.log(f"Worker出错: {e}") time.sleep(1) @@ -186,12 +192,12 @@ class BrowserWorkerPool: print(f"[浏览器池] {message}") def initialize(self): - """初始化工作线程池""" + """初始化工作线程池(按需模式,启动时不创建浏览器)""" with self.lock: if self.initialized: return - self.log(f"正在初始化工作线程池({self.pool_size}个worker)...") + self.log(f"正在初始化工作线程池({self.pool_size}个worker,按需启动浏览器)...") for i in range(self.pool_size): worker = BrowserWorker( @@ -202,11 +208,8 @@ class BrowserWorkerPool: worker.start() self.workers.append(worker) - # 等待所有worker准备就绪 - time.sleep(2) - self.initialized = True - self.log(f"✓ 工作线程池初始化完成({self.pool_size}个worker已就绪)") + self.log(f"✓ 工作线程池初始化完成({self.pool_size}个worker就绪,浏览器将在有任务时按需启动)") def submit_task(self, task_func: Callable, callback: Callable, *args, **kwargs) -> bool: """ diff --git a/database.py b/database.py index 42063b8..4ec9e34 100755 --- a/database.py +++ b/database.py @@ -1013,6 +1013,10 @@ def update_system_config(max_concurrent=None, schedule_enabled=None, schedule_ti updates.append('max_concurrent_per_account = ?') params.append(max_concurrent_per_account) + if max_screenshot_concurrent is not None: + updates.append('max_screenshot_concurrent = ?') + params.append(max_screenshot_concurrent) + if schedule_weekdays is not None: updates.append('schedule_weekdays = ?') params.append(schedule_weekdays) diff --git a/templates/admin.html b/templates/admin.html index 123a4e6..c895f2e 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -579,26 +579,26 @@

系统并发配置

- +
- 说明:同时最多运行的账号数量。浏览任务使用API方式,资源占用极低;截图任务会启动浏览器。建议设置2-5。 + 说明:同时最多运行的账号数量。浏览任务使用API方式,资源占用极低。
- +
- 说明:单个账号同时最多运行的任务数量。建议设置1-2。 + 说明:单个账号同时最多运行的任务数量。
- +
- 说明:同时进行截图的最大数量。截图使用浏览器,建议设置2-3。 + 说明:同时进行截图的最大数量。每个浏览器约占用200MB内存。
@@ -1509,22 +1509,22 @@ const maxConcurrentPerAccount = parseInt(document.getElementById('maxConcurrentPerAccount').value); const maxScreenshotConcurrent = parseInt(document.getElementById('maxScreenshotConcurrent').value); - if (maxConcurrent < 1 || maxConcurrent > 20) { - showNotification('全局并发数必须在1-20之间', 'error'); + if (maxConcurrent < 1) { + showNotification('全局并发数必须大于0', 'error'); return; } - if (maxConcurrentPerAccount < 1 || maxConcurrentPerAccount > 5) { - showNotification('单账号并发数必须在1-5之间', 'error'); + if (maxConcurrentPerAccount < 1) { + showNotification('单账号并发数必须大于0', 'error'); return; } - if (maxScreenshotConcurrent < 1 || maxScreenshotConcurrent > 5) { - showNotification('截图并发数必须在1-5之间', 'error'); + if (maxScreenshotConcurrent < 1) { + showNotification('截图并发数必须大于0', 'error'); return; } - if (!confirm(`确定更新并发配置吗?\n\n全局并发数: ${maxConcurrent}\n单账号并发数: ${maxConcurrentPerAccount}\n\n建议:并发数影响任务执行速度,过高可能触发目标服务器限制。全局建议2-5,单账号建议1-2`)) return; + if (!confirm(`确定更新并发配置吗?\n\n全局并发数: ${maxConcurrent}\n单账号并发数: ${maxConcurrentPerAccount}\n截图并发数: ${maxScreenshotConcurrent}`)) return; try { const response = await fetch('/yuyx/api/system/config', {