diff --git a/.env.example b/.env.example index ec5bf0e..bdd8bf4 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ PORT=2048 # GUI 启动器默认端口配置 DEFAULT_FASTAPI_PORT=2048 -DEFAULT_CAMOUFOX_PORT=9222 +DEFAULT_CAMOUFOX_PORT=40222 # 流式代理服务配置 STREAM_PORT=3120 @@ -64,7 +64,7 @@ AUTO_CONFIRM_LOGIN=true # ============================================================================= # Camoufox WebSocket 端点 -# CAMOUFOX_WS_ENDPOINT=ws://127.0.0.1:9222 +# CAMOUFOX_WS_ENDPOINT=ws://127.0.0.1:40222 # 启动模式 (normal, headless, virtual_display, direct_debug_no_browser) LAUNCH_MODE=normal diff --git a/README.md b/README.md index 1efdd4b..ad93366 100644 --- a/README.md +++ b/README.md @@ -350,7 +350,7 @@ cp .env.example .env ### 端口配置 - **FastAPI 服务**: 默认端口 `2048` -- **Camoufox 调试**: 默认端口 `9222` +- **Camoufox 调试**: 默认端口 `40222` - **流式代理**: 默认端口 `3120` ## 🔧 高级功能 diff --git a/README_en.md b/README_en.md index 2b5e273..62f2efd 100644 --- a/README_en.md +++ b/README_en.md @@ -344,7 +344,7 @@ cp .env.example .env ### Port Configuration - **FastAPI Service**: Default port `2048` -- **Camoufox Debug**: Default port `9222` +- **Camoufox Debug**: Default port `40222` - **Streaming Proxy**: Default port `3120` ## 🔧 Advanced Features diff --git a/docs/multi-worker-guide.md b/docs/multi-worker-guide.md index cec36c1..065ae85 100644 --- a/docs/multi-worker-guide.md +++ b/docs/multi-worker-guide.md @@ -69,7 +69,7 @@ Worker 配置保存在 `data/workers.json`: ```json { "workers": [ - {"id": "w1", "profile": "account1.json", "port": 3001, "camoufox_port": 9223}, + {"id": "w1", "profile": "account1.json", "port": 3001, "camoufox_port": 40223}, {"id": "w2", "profile": "account2.json", "port": 3002, "camoufox_port": 9224} ], "settings": {"recovery_hours": 6} diff --git a/src/api/app.py b/src/api/app.py index 8c0f1fb..0947f75 100644 --- a/src/api/app.py +++ b/src/api/app.py @@ -14,7 +14,6 @@ from models import WebSocketConnectionManager from logger import initialize_logging, restore_streams from browser import _initialize_page_logic, _close_page_logic, load_excluded_models, _handle_initial_model_state_and_storage -import proxy from asyncio import Queue, Lock from . import auth_utils playwright_manager: Optional[AsyncPlaywright] = None @@ -82,24 +81,30 @@ async def _wait_for_port(port: int, timeout: float = 10.0, interval: float = 0.3 return False async def _start_stream_proxy(): + import proxy import server STREAM_PORT = os.environ.get('STREAM_PORT') if STREAM_PORT != '0': port = int(STREAM_PORT or 3120) STREAM_PROXY_SERVER_ENV = os.environ.get('UNIFIED_PROXY_CONFIG') or os.environ.get('HTTPS_PROXY') or os.environ.get('HTTP_PROXY') server.logger.info(f'Starting STREAM proxy on port {port} with upstream proxy: {STREAM_PROXY_SERVER_ENV}') - server.STREAM_QUEUE = multiprocessing.Queue() - server.STREAM_PROCESS = multiprocessing.Process(target=proxy.start, args=(server.STREAM_QUEUE, port, STREAM_PROXY_SERVER_ENV)) - server.STREAM_PROCESS.start() - server.logger.info('STREAM proxy process started. Waiting for port readiness...') - if await _wait_for_port(port): - server.logger.info(f'STREAM proxy port {port} is ready.') - else: - server.logger.error(f'STREAM proxy port {port} not ready after timeout. Browser may fail to connect.') - if server.STREAM_PROCESS and server.STREAM_PROCESS.is_alive(): - server.logger.warning('STREAM proxy process is alive but port not listening.') + for attempt in range(3): + current_port = port + attempt + server.STREAM_QUEUE = multiprocessing.Queue() + server.STREAM_PROCESS = multiprocessing.Process(target=proxy.start, args=(server.STREAM_QUEUE, current_port, STREAM_PROXY_SERVER_ENV)) + server.STREAM_PROCESS.start() + server.logger.info(f'STREAM proxy process started on port {current_port}. Waiting for port readiness...') + if await _wait_for_port(current_port, timeout=30.0): + server.STREAM_PORT_ACTUAL = current_port + server.logger.info(f'STREAM proxy port {current_port} is ready.') + if current_port != port: + server.logger.warning(f'STREAM proxy using fallback port {current_port} (requested {port}).') + return else: - server.logger.error(f'STREAM proxy process died. Exit code: {server.STREAM_PROCESS.exitcode}') + server.logger.warning(f'STREAM proxy port {current_port} not ready, killing process...') + server.STREAM_PROCESS.terminate() + server.STREAM_PROCESS.join(timeout=3) + server.logger.error(f'STREAM proxy failed to start after 3 attempts.') async def _initialize_browser_and_page(): import server @@ -153,7 +158,7 @@ async def _shutdown_resources(): async def lifespan(app: FastAPI): """FastAPI application life cycle management""" import server - from server import queue_worker + from api import queue_worker original_streams = (sys.stdout, sys.stderr) initial_stdout, initial_stderr = _setup_logging() logger = server.logger @@ -175,7 +180,12 @@ async def lifespan(app: FastAPI): server.is_initializing = False yield except Exception as e: - logger.critical(f'Application startup failed: {e}', exc_info=True) + if 'Target page, context or browser has been closed' in str(e): + logger.warning(f'Application startup failed (browser closed): {e}') + elif 'NS_ERROR_PROXY' in str(e) or 'PROXY_CONNECTION_REFUSED' in str(e): + logger.warning(f'Application startup failed (proxy error): {e}') + else: + logger.critical(f'Application startup failed: {e}', exc_info=True) await _shutdown_resources() raise RuntimeError(f'Application startup failed: {e}') from e finally: diff --git a/src/api/queue_worker.py b/src/api/queue_worker.py index d0b6e9a..ced89eb 100644 --- a/src/api/queue_worker.py +++ b/src/api/queue_worker.py @@ -205,11 +205,11 @@ async def non_streaming_disconnect_monitor(): try: if completion_event: - from server import RESPONSE_COMPLETION_TIMEOUT + from config import RESPONSE_COMPLETION_TIMEOUT await asyncio.wait_for(completion_event.wait(), timeout=RESPONSE_COMPLETION_TIMEOUT / 1000 + 60) logger.info(f'[{req_id}] (Worker) ✅ 流式生成器完成信号收到。客户端提前断开: {client_disconnected_early}') else: - from server import RESPONSE_COMPLETION_TIMEOUT + from config import RESPONSE_COMPLETION_TIMEOUT await asyncio.wait_for(asyncio.shield(result_future), timeout=RESPONSE_COMPLETION_TIMEOUT / 1000 + 60) logger.info(f'[{req_id}] (Worker) ✅ 非流式处理完成。客户端提前断开: {client_disconnected_early}') diff --git a/src/api/request_processor.py b/src/api/request_processor.py index 39a455e..cc91024 100644 --- a/src/api/request_processor.py +++ b/src/api/request_processor.py @@ -15,7 +15,7 @@ from models import ChatCompletionRequest, ClientDisconnectedError from browser import switch_ai_studio_model, save_error_snapshot from .utils import validate_chat_request, prepare_combined_prompt, generate_sse_chunk, generate_sse_stop_chunk, use_stream_response, calculate_usage_stats, request_manager, calculate_stream_max_retries -from .abort_detector import AbortSignalDetector, AbortSignalHandler +from .abort_detector import AbortSignalHandler from browser.page_controller import PageController TOOL_CALL_INSTRUCTION = """When you need to call a tool, you MUST use EXACTLY this format (one per tool call): @@ -114,7 +114,13 @@ async def _analyze_model_requirements(req_id: str, context: dict, request: ChatC if parsed_model_list: valid_model_ids = [m.get('id') for m in parsed_model_list] if requested_model_id not in valid_model_ids: - raise HTTPException(status_code=400, detail=f"[{req_id}] Invalid model '{requested_model_id}'. Available models: {', '.join(valid_model_ids)}") + # fuzzy match: find model whose id contains the requested id or vice versa + fuzzy = next((mid for mid in valid_model_ids if requested_model_id in mid or mid.startswith(requested_model_id.split('-preview')[0])), None) + if fuzzy: + logger.info(f'[{req_id}] 模型 "{requested_model_id}" 不在列表中,自动映射到 "{fuzzy}"') + requested_model_id = fuzzy + else: + raise HTTPException(status_code=400, detail=f"[{req_id}] Invalid model '{requested_model_id}'. Available models: {', '.join(valid_model_ids)}") context['model_id_to_use'] = requested_model_id if current_ai_studio_model_id != requested_model_id: context['needs_model_switching'] = True diff --git a/src/browser/initialization.py b/src/browser/initialization.py index 9a42e98..5a500fc 100644 --- a/src/browser/initialization.py +++ b/src/browser/initialization.py @@ -312,12 +312,12 @@ async def _initialize_page_logic(browser: AsyncBrowser): if wrapper_locator: logger.info(f'✅ 输入框wrapper可见 (匹配: {wrapper_matched})') else: - logger.warning('⚠️ 未找到任何wrapper,尝试直接查找输入框') - input_locator, matched = await wait_for_any_selector(found_page, PROMPT_TEXTAREA_SELECTORS, timeout=10000) + logger.debug('⚠️ 未找到任何wrapper,尝试直接查找输入框') + input_locator, matched = await wait_for_any_selector(found_page, PROMPT_TEXTAREA_SELECTORS, timeout=30000) if input_locator: logger.info(f'✅ 核心输入区域可见 (匹配: {matched})') else: - await expect_async(found_page.locator(INPUT_SELECTOR)).to_be_visible(timeout=10000) + await expect_async(found_page.locator(INPUT_SELECTOR)).to_be_visible(timeout=30000) logger.info('✅ 核心输入区域可见 (默认选择器)') try: from config.selectors import MODEL_SELECTORS_LIST @@ -338,21 +338,33 @@ async def _initialize_page_logic(browser: AsyncBrowser): logger.info(f'✅ 页面逻辑初始化成功。') return (result_page_instance, result_page_ready) except Exception as input_visible_err: + from playwright._impl._errors import TargetClosedError + if isinstance(input_visible_err, TargetClosedError) or 'Target page, context or browser has been closed' in str(input_visible_err): + logger.warning(f'页面初始化时浏览器已关闭,跳过。') + raise from .operations import save_error_snapshot await save_error_snapshot('init_fail_input_timeout') logger.error(f'页面初始化失败:核心输入区域未在预期时间内变为可见。最后的 URL 是 {found_page.url}', exc_info=True) raise RuntimeError(f'页面初始化失败:核心输入区域未在预期时间内变为可见。最后的 URL 是 {found_page.url}') from input_visible_err except Exception as e_init_page: - logger.critical(f'❌ 页面逻辑初始化期间发生严重意外错误: {e_init_page}', exc_info=True) + is_browser_closed = 'Target page, context or browser has been closed' in str(e_init_page) + is_proxy_error = 'NS_ERROR_PROXY' in str(e_init_page) or 'PROXY_CONNECTION_REFUSED' in str(e_init_page) + if is_browser_closed: + logger.warning(f'页面初始化时浏览器已关闭: {e_init_page}') + elif is_proxy_error: + logger.warning(f'页面初始化时代理连接失败: {e_init_page}') + else: + logger.critical(f'❌ 页面逻辑初始化期间发生严重意外错误: {e_init_page}', exc_info=True) if temp_context: try: logger.info(f' 尝试关闭临时的浏览器上下文 due to initialization error.') await temp_context.close() logger.info(' ✅ 临时浏览器上下文已关闭。') except Exception as close_err: - logger.warning(f' ⚠️ 关闭临时浏览器上下文时出错: {close_err}') - from .operations import save_error_snapshot - await save_error_snapshot('init_unexpected_error') + logger.debug(f' 关闭临时浏览器上下文时出错: {close_err}') + if not is_browser_closed: + from .operations import save_error_snapshot + await save_error_snapshot('init_unexpected_error') raise RuntimeError(f'页面初始化意外错误: {e_init_page}') from e_init_page async def _close_page_logic(): diff --git a/src/browser/model_management.py b/src/browser/model_management.py index 2ac7d30..1a34849 100644 --- a/src/browser/model_management.py +++ b/src/browser/model_management.py @@ -29,7 +29,10 @@ async def _verify_ui_state_settings(page: AsyncPage, req_id: str='unknown') -> d logger.error(f'[{req_id}] ❌ 解析localStorage JSON失败: {e}') return {'exists': False, 'isAdvancedOpen': None, 'areToolsOpen': None, 'needsUpdate': True, 'error': f'JSON解析失败: {e}'} except Exception as e: - logger.error(f'[{req_id}] ❌ 验证UI状态设置时发生错误: {e}') + if 'Target page, context or browser has been closed' in str(e): + logger.debug(f'[{req_id}] UI状态验证时浏览器已关闭') + else: + logger.error(f'[{req_id}] ❌ 验证UI状态设置时发生错误: {e}') return {'exists': False, 'isAdvancedOpen': None, 'areToolsOpen': None, 'needsUpdate': True, 'error': f'验证失败: {e}'} async def _force_ui_state_settings(page: AsyncPage, req_id: str='unknown') -> bool: @@ -84,7 +87,10 @@ async def _force_ui_state_settings(page: AsyncPage, req_id: str='unknown') -> bo return False except Exception as e: - logger.error(f'[{req_id}] ❌ 强制设置UI状态错误: {e}') + if 'Target page, context or browser has been closed' in str(e): + logger.debug(f'[{req_id}] 强制设置UI时浏览器已关闭') + else: + logger.error(f'[{req_id}] ❌ 强制设置UI状态错误: {e}') return False async def _force_ui_state_with_retry(page: AsyncPage, req_id: str='unknown', max_retries: int=3, retry_delay: float=1.0) -> bool: @@ -385,7 +391,7 @@ async def _handle_initial_model_state_and_storage(page: AsyncPage): except Exception as reload_err: err_str = str(reload_err) if 'Target page, context or browser has been closed' in err_str or 'Browser has been closed' in err_str: - logger.warning(f' ⚠️ 浏览器已关闭,跳过重新加载。') + logger.debug(f' ⚠️ 浏览器已关闭,跳过重新加载。') return logger.warning(f' ⚠️ 页面重新加载尝试 {attempt + 1}/{max_retries} 失败: {reload_err}') if attempt < max_retries - 1: @@ -401,6 +407,10 @@ async def _handle_initial_model_state_and_storage(page: AsyncPage): else: logger.info(' localStorage 状态良好 (isAdvancedOpen=true, promptModel有效),无需刷新页面。') except Exception as e: + from playwright._impl._errors import TargetClosedError + if isinstance(e, TargetClosedError): + logger.debug(f'处理初始模型状态时浏览器已关闭: {e}') + return logger.error(f'❌ (新) 处理初始模型状态和 localStorage 时发生严重错误: {e}', exc_info=True) try: logger.warning(' 由于发生错误,尝试回退仅从页面显示设置全局模型 ID (不写入localStorage)...') @@ -421,7 +431,7 @@ async def _set_model_from_page_display(page: AsyncPage, set_storage: bool=False) ) if not displayed_model_name: - logger.warning(' 所有选择器都无法获取页面显示的模型名称') + logger.debug(' 所有选择器都无法获取页面显示的模型名称') displayed_model_name = '未知模型' found_model_id_from_display = None if model_list_fetch_event and (not model_list_fetch_event.is_set()): @@ -437,7 +447,7 @@ async def _set_model_from_page_display(page: AsyncPage, set_storage: bool=False) logger.info(f" 显示名称 '{displayed_model_name}' 对应模型 ID: {found_model_id_from_display}") break if not found_model_id_from_display: - logger.warning(f" 未在已知模型列表中找到与显示名称 '{displayed_model_name}' 匹配的 ID。") + logger.debug(f" 未在已知模型列表中找到与显示名称 '{displayed_model_name}' 匹配的 ID。") else: logger.warning(' 模型列表尚不可用,无法将显示名称转换为ID。') new_model_value = found_model_id_from_display if found_model_id_from_display else displayed_model_name @@ -478,4 +488,8 @@ async def _set_model_from_page_display(page: AsyncPage, set_storage: bool=False) await page.evaluate("(prefsStr) => localStorage.setItem('aiStudioUserPreference', prefsStr)", json.dumps(prefs_to_set)) logger.info(f" ✅ localStorage.aiStudioUserPreference 已更新。isAdvancedOpen: {prefs_to_set.get('isAdvancedOpen')}, areToolsOpen: {prefs_to_set.get('areToolsOpen')} (期望: True), promptModel: '{prefs_to_set.get('promptModel', '未设置/保留原样')}'。") except Exception as e_set_disp: - logger.error(f' 尝试从页面显示设置模型时出错: {e_set_disp}', exc_info=True) \ No newline at end of file + from playwright._impl._errors import TargetClosedError + if isinstance(e_set_disp, TargetClosedError): + logger.debug(f' 尝试从页面显示设置模型时出错 (browser closed): {e_set_disp}') + else: + logger.error(f' 尝试从页面显示设置模型时出错: {e_set_disp}', exc_info=True) \ No newline at end of file diff --git a/src/browser/operations.py b/src/browser/operations.py index 23a2873..0db9e70 100644 --- a/src/browser/operations.py +++ b/src/browser/operations.py @@ -4,8 +4,7 @@ import os import re import logging -import functools -from typing import Optional, Any, List, Dict, Callable, Set, TypeVar +from typing import Optional, Any, Callable, TypeVar from playwright.async_api import Page as AsyncPage, Locator, Error as PlaywrightAsyncError from config import * from models import ClientDisconnectedError, ElementClickError diff --git a/src/browser/page_controller.py b/src/browser/page_controller.py index 8a6b4b1..ccb37ae 100644 --- a/src/browser/page_controller.py +++ b/src/browser/page_controller.py @@ -376,6 +376,9 @@ async def _adjust_google_search(self, request_params: Dict[str, Any], check_clie for attempt in range(1, max_retries + 1): try: toggle_locator = self.page.locator(toggle_selector) + if await toggle_locator.count() == 0: + self.logger.debug(f'[{self.req_id}] Google Search 开关不存在,跳过。') + return await expect_async(toggle_locator).to_be_visible(timeout=5000) await self._check_disconnect(check_client_disconnected, 'Google Search 開關 - 元素可見後') is_checked_str = await toggle_locator.get_attribute('aria-checked') @@ -385,17 +388,24 @@ async def _adjust_google_search(self, request_params: Dict[str, Any], check_clie return action = '打開' if should_enable_search else '關閉' self.logger.info(f'[{self.req_id}] 🌍 (嘗試 {attempt}/{max_retries}) 正在{action} Google Search...') - await click_element(self.page, toggle_locator, 'Google Search Toggle', self.req_id) + await toggle_locator.scroll_into_view_if_needed(timeout=3000) + await toggle_locator.click(force=True, timeout=3000) await self._check_disconnect(check_client_disconnected, f'Google Search 開關 - 點擊{action}後') - await asyncio.sleep(SLEEP_LONG) + await asyncio.sleep(1.0) new_state = await toggle_locator.get_attribute('aria-checked') if (new_state == 'true') == should_enable_search: self.logger.info(f'[{self.req_id}] ✅ Google Search 已{action}。') return - else: - self.logger.warning(f"[{self.req_id}] ⚠️ Google Search {action}失敗 (嘗試 {attempt}): '{new_state}'") - if attempt < max_retries: - await asyncio.sleep(DELAY_AFTER_TOGGLE) + # Force via JS click on parent label + await toggle_locator.evaluate('el => (el.closest("label") || el).click()') + await asyncio.sleep(1.0) + new_state = await toggle_locator.get_attribute('aria-checked') + if (new_state == 'true') == should_enable_search: + self.logger.info(f'[{self.req_id}] ✅ Google Search 已{action} (JS)。') + return + self.logger.warning(f"[{self.req_id}] ⚠️ Google Search {action}失敗 (嘗試 {attempt}): '{new_state}'") + if attempt < max_retries: + await asyncio.sleep(DELAY_AFTER_TOGGLE) except Exception as e: if isinstance(e, ClientDisconnectedError): raise @@ -467,6 +477,9 @@ async def _ensure_tools_panel_expanded(self, check_client_disconnected: Callable for attempt in range(1, max_retries + 1): try: collapse_tools_locator = self.page.locator('button[aria-label="Expand or collapse tools"]') + if await collapse_tools_locator.count() == 0: + self.logger.info(f'[{self.req_id}] 工具面板展开按钮不存在,跳过。') + return await expect_async(collapse_tools_locator).to_be_visible(timeout=5000) grandparent_locator = collapse_tools_locator.locator('xpath=../..') class_string = await grandparent_locator.get_attribute('class', timeout=3000) @@ -563,11 +576,18 @@ async def _control_thinking_budget_toggle(self, should_be_checked: bool, check_c async def _set_parameter_with_retry(self, locator: Locator, target_value: str, param_name: str, check_client_disconnected: Callable) -> bool: def is_equal(val1, val2): + import re + def extract_float(s): + m = re.search(r'[-+]?\d*\.?\d+', str(s)) + return float(m.group()) if m else None try: - f1, f2 = float(val1), float(val2) - return abs(f1 - f2) < 0.001 - except ValueError: - return str(val1).strip() == str(val2).strip() + f1 = extract_float(val1) + f2 = float(val2) + if f1 is not None: + return abs(f1 - f2) < 0.001 + except (ValueError, TypeError): + pass + return str(val1).strip() == str(val2).strip() max_retries = MAX_RETRIES for attempt in range(max_retries): @@ -576,32 +596,39 @@ def is_equal(val1, val2): await self._check_disconnect(check_client_disconnected, f'设置 {param_name} - 尝试 {attempt + 1}') if attempt == 0: - await expect_async(locator).to_be_visible(timeout=5000) + try: + await expect_async(locator).to_be_visible(timeout=5000) + except Exception: + # Panel might be collapsed - try to expand and retry visibility + await self._ensure_advanced_settings_expanded(check_client_disconnected) + await expect_async(locator).to_be_visible(timeout=5000) await asyncio.sleep(DELAY_AFTER_TOGGLE) if attempt == 0: strategy_name = "JS Injection" - await locator.evaluate('(el, val) => { el.value = val; el.dispatchEvent(new Event("input", {bubbles: true})); el.dispatchEvent(new Event("change", {bubbles: true})); }', str(target_value)) + await locator.evaluate('(el, val) => { el.value = val; el.dispatchEvent(new Event("input", {bubbles: true})); el.dispatchEvent(new Event("change", {bubbles: true})); el.dispatchEvent(new Event("blur", {bubbles: true})); }', str(target_value)) await asyncio.sleep(DELAY_AFTER_FILL) - await locator.press('Enter') + await locator.press('Tab') elif attempt == 1: - strategy_name = "Standard Fill" - await locator.focus() + strategy_name = "Ctrl+A Fill" + await locator.click() + await locator.press('Control+a') await locator.fill(str(target_value)) + await locator.dispatch_event('input') await locator.dispatch_event('change') - await locator.press('Enter') + await locator.press('Tab') + await asyncio.sleep(DELAY_AFTER_FILL) else: - strategy_name = "Select & Type" - await locator.focus() - await locator.select_text() - await locator.press('Backspace') - await asyncio.sleep(SLEEP_TICK) - await locator.type(str(target_value), delay=50) - await locator.press('Enter') + strategy_name = "Triple Click Fill" + await locator.click(click_count=3) + await locator.fill(str(target_value)) + await locator.dispatch_event('input') + await locator.dispatch_event('change') + await locator.press('Tab') await asyncio.sleep(SLEEP_LONG) - final_val = await locator.input_value(timeout=2000) + final_val = await locator.input_value(timeout=5000) if is_equal(final_val, target_value): self.logger.info(f"[{self.req_id}] {param_name} 成功设置为 {final_val} (策略: {strategy_name})。") return True @@ -693,10 +720,16 @@ async def _adjust_stop_sequences(self, stop_sequences, page_params_cache: dict, except Exception: break if normalized_requested_stops: - await expect_async(stop_input_locator).to_be_visible(timeout=5000) + try: + await expect_async(stop_input_locator).to_be_visible(timeout=5000) + except Exception: + await self._ensure_advanced_settings_expanded(check_client_disconnected) + await expect_async(stop_input_locator).to_be_visible(timeout=5000) + await stop_input_locator.scroll_into_view_if_needed(timeout=3000) for seq in normalized_requested_stops: - await stop_input_locator.fill(seq, timeout=3000) - await stop_input_locator.press('Enter', timeout=3000) + await stop_input_locator.click(timeout=3000) + await stop_input_locator.fill(seq, timeout=5000) + await stop_input_locator.press('Enter', timeout=5000) await asyncio.sleep(DELAY_AFTER_FILL) page_params_cache['stop_sequences'] = normalized_requested_stops self.logger.info(f'[{self.req_id}] 停止序列已成功设置。缓存已更新。') @@ -750,8 +783,16 @@ async def _verify_chat_cleared(self, check_client_disconnected: Callable): await expect_async(self.page).to_have_url(re.compile('.*/prompts/new_chat.*'), timeout=CLEAR_CHAT_VERIFY_TIMEOUT_MS) self.logger.info(f'[{self.req_id}] - URL验证成功: 页面已导航到 new_chat。') zero_state_locator = self.page.locator(ZERO_STATE_SELECTOR) - await expect_async(zero_state_locator).to_be_visible(timeout=5000) - self.logger.info(f'[{self.req_id}] - UI验证成功: “零状态”元素可见。') + try: + await expect_async(zero_state_locator).to_be_visible(timeout=5000) + self.logger.info(f'[{self.req_id}] - UI验证成功: "零状态"元素可见。') + except Exception: + self.logger.debug(f'[{self.req_id}] - zero_state not visible, waiting for textarea...') + try: + await expect_async(self.page.locator(PROMPT_TEXTAREA_SELECTOR)).to_be_visible(timeout=10000) + self.logger.info(f'[{self.req_id}] - Textarea visible, page ready.') + except Exception: + self.logger.debug(f'[{self.req_id}] - Textarea also not visible, continuing anyway.') self.logger.info(f'[{self.req_id}] 聊天已成功清空 (验证通过)。') except Exception as verify_err: self.logger.error(f'[{self.req_id}] 错误: 清空聊天验证失败: {verify_err}') @@ -1003,7 +1044,7 @@ async def _paste_images_via_event(self, images: List[Dict[str, str]], target_loc async def submit_prompt(self, prompt: str, image_list: List, check_client_disconnected: Callable): self.logger.info(f'[{self.req_id}] 📤 提交提示 ({len(prompt)} chars)...') - prompt_textarea_locator, matched_selector = await get_first_visible_locator(self.page, PROMPT_TEXTAREA_SELECTORS, timeout=5000) + prompt_textarea_locator, matched_selector = await get_first_visible_locator(self.page, PROMPT_TEXTAREA_SELECTORS, timeout=15000) if not prompt_textarea_locator: self.logger.warning(f'[{self.req_id}] 未找到输入框,尝试默认选择器') prompt_textarea_locator = self.page.locator(PROMPT_TEXTAREA_SELECTOR) @@ -1070,9 +1111,14 @@ async def submit_prompt(self, prompt: str, image_list: List, check_client_discon await asyncio.sleep(SLEEP_TICK) submitted_successfully = await self._try_shortcut_submit(prompt_textarea_locator, check_client_disconnected) if not submitted_successfully: - self.logger.info(f'[{self.req_id}] 快捷键提交失败,尝试点击提交按钮...') - await click_element(self.page, submit_button_locator, 'Submit Button', self.req_id) - self.logger.info(f'[{self.req_id}] 提交按钮点击完成。') + # Check if response already started (submission may have succeeded despite verification failure) + response_container = self.page.locator(RESPONSE_CONTAINER_SELECTOR) + if await response_container.count() > 0 and await response_container.last.is_visible(timeout=2000): + self.logger.info(f'[{self.req_id}] 快捷键验证失败但响应已开始,视为提交成功。') + else: + self.logger.info(f'[{self.req_id}] 快捷键提交失败,尝试点击提交按钮...') + await click_element(self.page, submit_button_locator, 'Submit Button', self.req_id, internal_timeout=10000) + self.logger.info(f'[{self.req_id}] 提交按钮点击完成。') await self._check_disconnect(check_client_disconnected, '提交后') except Exception as e_input_submit: @@ -1150,20 +1196,28 @@ async def _verify_images_uploaded(self, expected_count: int, check_client_discon async def _verify_submission(self, prompt_textarea_locator: Locator, original_content: str) -> bool: try: - current_content = await prompt_textarea_locator.last.input_value(timeout=1500) or '' + current_content = await prompt_textarea_locator.last.input_value(timeout=3000) or '' if original_content and not current_content.strip(): self.logger.info(f'[{self.req_id}] Verification Method 1: Textarea cleared, submission successful.') return True submit_button_locator = self.page.locator(SUBMIT_BUTTON_SELECTOR) - if await submit_button_locator.is_disabled(timeout=1500): + if await submit_button_locator.is_disabled(timeout=3000): self.logger.info(f'[{self.req_id}] Verification Method 2: Submit button is disabled, submission successful.') return True response_container = self.page.locator(RESPONSE_CONTAINER_SELECTOR) - if await response_container.count() > 0 and await response_container.last.is_visible(timeout=1000): + if await response_container.count() > 0 and await response_container.last.is_visible(timeout=2000): self.logger.info(f'[{self.req_id}] Verification Method 3: New response container detected, submission successful.') return True except Exception as verify_err: self.logger.warning(f'[{self.req_id}] Could not confirm submission during verification: {type(verify_err).__name__}') + # Even if verification timed out, check if response already started + try: + response_container = self.page.locator(RESPONSE_CONTAINER_SELECTOR) + if await response_container.count() > 0 and await response_container.last.is_visible(timeout=1000): + self.logger.info(f'[{self.req_id}] Verification fallback: Response container visible, submission successful.') + return True + except Exception: + pass return False return False @@ -1189,12 +1243,12 @@ async def _try_shortcut_submit(self, prompt_textarea_locator, check_client_disco user_agent_data_platform = 'Other' is_mac_determined = 'mac' in user_agent_data_platform.lower() shortcut_modifier = 'Meta' if is_mac_determined else 'Control' - await prompt_textarea_locator.focus(timeout=5000) + await prompt_textarea_locator.focus(timeout=15000) await self._check_disconnect(check_client_disconnected, 'After Input Focus') original_content = await prompt_textarea_locator.input_value(timeout=2000) or '' self.logger.info(f'[{self.req_id}] - Attempting {shortcut_modifier}+Enter...') await self.page.keyboard.press(f'{shortcut_modifier}+Enter') - await asyncio.sleep(1.5) + await asyncio.sleep(2.5) if await self._verify_submission(prompt_textarea_locator, original_content): self.logger.info(f'[{self.req_id}] ✅ Success with {shortcut_modifier}+Enter.') return True @@ -1202,7 +1256,7 @@ async def _try_shortcut_submit(self, prompt_textarea_locator, check_client_disco self.logger.info(f'[{self.req_id}] - Attempting Enter...') await prompt_textarea_locator.focus(timeout=5000) await self.page.keyboard.press('Enter') - await asyncio.sleep(1.5) + await asyncio.sleep(2.5) if await self._verify_submission(prompt_textarea_locator, original_content): self.logger.info(f'[{self.req_id}] ✅ Success with Enter.') return True diff --git a/src/config/timeouts.py b/src/config/timeouts.py index bffcbca..cefa24c 100644 --- a/src/config/timeouts.py +++ b/src/config/timeouts.py @@ -25,7 +25,7 @@ SLEEP_TICK = 0.1 SLEEP_SHORT = 0.15 SLEEP_MEDIUM = 0.25 -SLEEP_LONG = 0.5 +SLEEP_LONG = 1.0 SLEEP_RETRY = 1.0 SLEEP_NAVIGATION = 2.0 SLEEP_VIDEO_POLL = 5.0 diff --git a/src/launch_camoufox.py b/src/launch_camoufox.py index 60b3ff0..c83768b 100644 --- a/src/launch_camoufox.py +++ b/src/launch_camoufox.py @@ -34,7 +34,7 @@ PYTHON_EXECUTABLE = sys.executable ENDPOINT_CAPTURE_TIMEOUT = int(os.environ.get('ENDPOINT_CAPTURE_TIMEOUT', '90')) DEFAULT_SERVER_PORT = int(os.environ.get('DEFAULT_FASTAPI_PORT', '2048')) -DEFAULT_CAMOUFOX_PORT = int(os.environ.get('DEFAULT_CAMOUFOX_PORT', '9222')) +DEFAULT_CAMOUFOX_PORT = int(os.environ.get('DEFAULT_CAMOUFOX_PORT', '40222')) DEFAULT_STREAM_PORT = int(os.environ.get('STREAM_PORT', '3120')) DEFAULT_HELPER_ENDPOINT = os.environ.get('GUI_DEFAULT_HELPER_ENDPOINT', '') DEFAULT_AUTH_SAVE_TIMEOUT = int(os.environ.get('AUTH_SAVE_TIMEOUT', '30')) @@ -499,8 +499,26 @@ def determine_proxy_configuration(internal_camoufox_proxy_arg=None): 'network.dns.disablePrefetch': True, # Reduce font memory 'gfx.font_rendering.fontconfig.max_generic_substitutions': 3, + # Disable GPU / hardware acceleration + 'layers.acceleration.disabled': True, + 'gfx.direct2d.disabled': True, + 'media.hardware-video-decoding.enabled': False, + # Disable WebGL (not needed for AI Studio) + 'webgl.disabled': True, + 'webgl.enable-webgl2': False, + # Reduce HTTP connections + 'network.http.max-connections': 6, + 'network.http.max-persistent-connections-per-server': 2, + # Disable push / notifications + 'dom.push.enabled': False, + 'dom.webnotifications.enabled': False, + # Reduce session store overhead + 'browser.sessionstore.resume_from_crash': False, + 'browser.sessionstore.interval': 600000, + # Reduce WebSocket connection limit + 'network.websocket.max-connections': 10, } - launch_args_for_internal_camoufox = {'port': camoufox_port_internal, 'addons': [], 'exclude_addons': [DefaultAddons.UBO], 'window': (1280, 720), 'firefox_user_prefs': memory_optimization_prefs} + launch_args_for_internal_camoufox = {'port': camoufox_port_internal, 'addons': [], 'exclude_addons': [DefaultAddons.UBO], 'window': (1024, 600), 'firefox_user_prefs': memory_optimization_prefs} if camoufox_proxy_internal: launch_args_for_internal_camoufox['proxy'] = {'server': camoufox_proxy_internal} if auth_file: @@ -719,55 +737,88 @@ def determine_proxy_configuration(internal_camoufox_proxy_arg=None): camoufox_popen_kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW try: logger.info(f" 将执行 Camoufox 内部启动命令: {' '.join(camoufox_internal_cmd_args)}") - camoufox_proc = subprocess.Popen(camoufox_internal_cmd_args, **camoufox_popen_kwargs) - logger.info(f' Camoufox 内部进程已启动 (PID: {camoufox_proc.pid})。正在等待 WebSocket 端点输出 (最长 {ENDPOINT_CAPTURE_TIMEOUT} 秒)...') - camoufox_output_q = queue.Queue() - camoufox_stdout_reader = threading.Thread(target=_enqueue_output, args=(camoufox_proc.stdout, 'stdout', camoufox_output_q, camoufox_proc.pid), daemon=True) - camoufox_stderr_reader = threading.Thread(target=_enqueue_output, args=(camoufox_proc.stderr, 'stderr', camoufox_output_q, camoufox_proc.pid), daemon=True) - camoufox_stdout_reader.start() - camoufox_stderr_reader.start() - ws_capture_start_time = time.time() - camoufox_ended_streams_count = 0 - while time.time() - ws_capture_start_time < ENDPOINT_CAPTURE_TIMEOUT: - if camoufox_proc.poll() is not None: - logger.error(f' Camoufox 内部进程 (PID: {camoufox_proc.pid}) 在等待 WebSocket 端点期间已意外退出,退出码: {camoufox_proc.poll()}。') - break - try: - stream_name, line_from_camoufox = camoufox_output_q.get(timeout=0.2) - if line_from_camoufox is None: - camoufox_ended_streams_count += 1 - logger.debug(f' [InternalCamoufox-{stream_name}-PID:{camoufox_proc.pid}] 输出流已关闭 (EOF)。') - if camoufox_ended_streams_count >= 2: - logger.info(f' Camoufox 内部进程 (PID: {camoufox_proc.pid}) 的所有输出流均已关闭。') - break + def _find_free_port(start_port: int, max_tries: int = 20) -> int: + for p in range(start_port, start_port + max_tries): + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 0) + s.bind(('127.0.0.1', p)) + return p + except OSError: continue - log_line_content = f'[InternalCamoufox-{stream_name}-PID:{camoufox_proc.pid}]: {line_from_camoufox.rstrip()}' - if stream_name == 'stderr' or 'ERROR' in line_from_camoufox.upper() or '❌' in line_from_camoufox: - logger.warning(log_line_content) - else: - logger.info(log_line_content) - ws_match = ws_regex.search(line_from_camoufox) - if ws_match: - captured_ws_endpoint = ws_match.group(1) - logger.info(f' ✅ 成功从 Camoufox 内部进程捕获到 WebSocket 端点: {captured_ws_endpoint[:40]}...') + return start_port + max_tries + MAX_CAMOUFOX_RETRIES = 3 + for camoufox_attempt in range(MAX_CAMOUFOX_RETRIES): + # Always find a free port (avoids EADDRINUSE on first and retry attempts) + free_port = _find_free_port(args.camoufox_debug_port + camoufox_attempt) + for i, arg in enumerate(camoufox_internal_cmd_args): + if arg == '--internal-camoufox-port' and i + 1 < len(camoufox_internal_cmd_args): + camoufox_internal_cmd_args[i + 1] = str(free_port) break - except queue.Empty: - continue - if camoufox_stdout_reader.is_alive(): - camoufox_stdout_reader.join(timeout=1.0) - if camoufox_stderr_reader.is_alive(): - camoufox_stderr_reader.join(timeout=1.0) - if not captured_ws_endpoint and (camoufox_proc and camoufox_proc.poll() is None): - logger.error(f' ❌ 未能在 {ENDPOINT_CAPTURE_TIMEOUT} 秒内从 Camoufox 内部进程 (PID: {camoufox_proc.pid}) 捕获到 WebSocket 端点。') - logger.error(' Camoufox 内部进程仍在运行,但未输出预期的 WebSocket 端点。请检查其日志或行为。') + if camoufox_attempt > 0: + logger.warning(f' 🔄 重试启动 Camoufox (第 {camoufox_attempt + 1}/{MAX_CAMOUFOX_RETRIES} 次, 端口: {free_port})...') + else: + logger.info(f' 使用端口 {free_port} 启动 Camoufox...') + camoufox_proc = subprocess.Popen(camoufox_internal_cmd_args, **camoufox_popen_kwargs) + logger.info(f' Camoufox 内部进程已启动 (PID: {camoufox_proc.pid})。正在等待 WebSocket 端点输出 (最长 {ENDPOINT_CAPTURE_TIMEOUT} 秒)...') + camoufox_output_q = queue.Queue() + camoufox_stdout_reader = threading.Thread(target=_enqueue_output, args=(camoufox_proc.stdout, 'stdout', camoufox_output_q, camoufox_proc.pid), daemon=True) + camoufox_stderr_reader = threading.Thread(target=_enqueue_output, args=(camoufox_proc.stderr, 'stderr', camoufox_output_q, camoufox_proc.pid), daemon=True) + camoufox_stdout_reader.start() + camoufox_stderr_reader.start() + ws_capture_start_time = time.time() + camoufox_ended_streams_count = 0 + while time.time() - ws_capture_start_time < ENDPOINT_CAPTURE_TIMEOUT: + if camoufox_proc.poll() is not None: + logger.error(f' Camoufox 内部进程 (PID: {camoufox_proc.pid}) 在等待 WebSocket 端点期间已意外退出,退出码: {camoufox_proc.poll()}。') + break + try: + stream_name, line_from_camoufox = camoufox_output_q.get(timeout=0.2) + if line_from_camoufox is None: + camoufox_ended_streams_count += 1 + logger.debug(f' [InternalCamoufox-{stream_name}-PID:{camoufox_proc.pid}] 输出流已关闭 (EOF)。') + if camoufox_ended_streams_count >= 2: + logger.info(f' Camoufox 内部进程 (PID: {camoufox_proc.pid}) 的所有输出流均已关闭。') + break + continue + log_line_content = f'[InternalCamoufox-{stream_name}-PID:{camoufox_proc.pid}]: {line_from_camoufox.rstrip()}' + if stream_name == 'stderr' or 'ERROR' in line_from_camoufox.upper() or '❌' in line_from_camoufox: + logger.warning(log_line_content) + else: + logger.info(log_line_content) + # Early exit on port conflict - no need to wait full timeout + if 'EADDRINUSE' in line_from_camoufox: + logger.warning(f' ⚡ 检测到端口冲突,立即终止并重试...') + break + ws_match = ws_regex.search(line_from_camoufox) + if ws_match: + captured_ws_endpoint = ws_match.group(1) + logger.info(f' ✅ 成功从 Camoufox 内部进程捕获到 WebSocket 端点: {captured_ws_endpoint[:40]}...') + break + except queue.Empty: + continue + if camoufox_stdout_reader.is_alive(): + camoufox_stdout_reader.join(timeout=1.0) + if camoufox_stderr_reader.is_alive(): + camoufox_stderr_reader.join(timeout=1.0) + if captured_ws_endpoint: + break + # Failed - kill process and retry + if camoufox_proc and camoufox_proc.poll() is None: + logger.warning(f' ❌ 未能在 {ENDPOINT_CAPTURE_TIMEOUT} 秒内捕获到 WebSocket 端点,终止进程 (PID: {camoufox_proc.pid})...') + camoufox_proc.terminate() + try: + camoufox_proc.wait(timeout=5) + except subprocess.TimeoutExpired: + camoufox_proc.kill() + else: + logger.warning(f' ❌ Camoufox 内部进程已退出,未能捕获到 WebSocket 端点。') + if camoufox_attempt < MAX_CAMOUFOX_RETRIES - 1: + time.sleep(3) + if not captured_ws_endpoint: + logger.error(f' ❌ Camoufox 在 {MAX_CAMOUFOX_RETRIES} 次尝试后均未能输出 WebSocket 端点。') cleanup() sys.exit(1) - elif not captured_ws_endpoint and (camoufox_proc and camoufox_proc.poll() is not None): - logger.error(f' ❌ Camoufox 内部进程已退出,且未能捕获到 WebSocket 端点。') - sys.exit(1) - elif not captured_ws_endpoint: - logger.error(f' ❌ 未能捕获到 WebSocket 端点。') - sys.exit(1) except Exception as e_launch_camoufox_internal: logger.critical(f' ❌ 在内部启动 Camoufox 或捕获其 WebSocket 端点时发生致命错误: {e_launch_camoufox_internal}', exc_info=True) cleanup() diff --git a/src/manager/routes/system.py b/src/manager/routes/system.py index ff851d8..1da1422 100644 --- a/src/manager/routes/system.py +++ b/src/manager/routes/system.py @@ -16,7 +16,7 @@ async def check_all_ports(): config = manager.load_config() ports_to_check = [ {"label": "FastAPI 服务", "port": config.get("fastapi_port", 2048)}, - {"label": "Camoufox 调试", "port": config.get("camoufox_debug_port", 9222)}, + {"label": "Camoufox 调试", "port": config.get("camoufox_debug_port", 40222)}, ] if config.get("stream_port_enabled"): diff --git a/src/manager/routes/workers.py b/src/manager/routes/workers.py index 3928edc..e822636 100644 --- a/src/manager/routes/workers.py +++ b/src/manager/routes/workers.py @@ -34,7 +34,7 @@ async def add_worker(profile: str = Body(..., embed=True)): existing_ports = [worker.port for worker in pool.workers.values()] existing_camoufox_ports = [worker.camoufox_port for worker in pool.workers.values()] port = max(existing_ports, default=3000) + 1 - camoufox_port = max(existing_camoufox_ports, default=9221) + 1 + camoufox_port = max(existing_camoufox_ports, default=40221) + 1 profile_path = os.path.join(SAVED_AUTH_DIR, profile) if not os.path.exists(profile_path): diff --git a/src/manager/service.py b/src/manager/service.py index e2ce50e..77ed277 100644 --- a/src/manager/service.py +++ b/src/manager/service.py @@ -7,7 +7,6 @@ import sys import threading import time -from concurrent.futures import ThreadPoolExecutor from typing import Any, Dict, List, Optional from fastapi import WebSocket @@ -43,7 +42,7 @@ class ServiceManager: def __init__(self): self.process: Optional[subprocess.Popen] = None - self.log_queue: asyncio.Queue = asyncio.Queue() + self.log_queue: asyncio.Queue = asyncio.Queue(maxsize=1000) self.active_connections: List[WebSocket] = [] self.output_thread: Optional[threading.Thread] = None self.stop_event = threading.Event() @@ -66,7 +65,7 @@ def load_config(self) -> Dict[str, Any]: pass return { "fastapi_port": 2048, - "camoufox_debug_port": 9222, + "camoufox_debug_port": 40222, "stream_port": 3120, "stream_port_enabled": True, "proxy_enabled": False, @@ -362,7 +361,7 @@ def _start_single_mode( "--server-port", str(config.get("fastapi_port", 2048)), "--camoufox-debug-port", - str(config.get("camoufox_debug_port", 9222)), + str(config.get("camoufox_debug_port", 40222)), ] if config.get("proxy_enabled"): @@ -523,6 +522,7 @@ def kill_process(process: subprocess.Popen) -> None: if self.is_worker_mode: processes = list(self.worker_processes.values()) if processes: + from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor( max_workers=max(1, len(processes)) ) as executor: diff --git a/src/media/models.py b/src/media/models.py index 1b74b39..0cc2ed8 100644 --- a/src/media/models.py +++ b/src/media/models.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import Optional, List, Literal, Union +from typing import Optional, List, Union IMAGEN_SUPPORTED_MODELS = ['imagen-4.0-generate-001', 'imagen-4.0-ultra-generate-001', 'imagen-4.0-fast-generate-001'] VEO_SUPPORTED_MODELS = ['veo-2.0-generate-001'] diff --git a/src/media/nano_controller.py b/src/media/nano_controller.py index 1e068a5..6e48741 100644 --- a/src/media/nano_controller.py +++ b/src/media/nano_controller.py @@ -1,5 +1,4 @@ import asyncio -import base64 from typing import Callable, Optional, List from playwright.async_api import Page as AsyncPage, Locator, expect as expect_async from config.nano_selectors import ( diff --git a/src/models/types.py b/src/models/types.py index c33b77d..447a812 100644 --- a/src/models/types.py +++ b/src/models/types.py @@ -1,6 +1,6 @@ from typing import Any, Dict, List, Optional, Union -from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic import BaseModel, ConfigDict, model_validator from config import MODEL_NAME diff --git a/src/server.py b/src/server.py index 75470f4..2c4f8d4 100644 --- a/src/server.py +++ b/src/server.py @@ -1,40 +1,21 @@ import asyncio import multiprocessing -import random -import time -import json -from typing import List, Optional, Dict, Any, Union, AsyncGenerator, Tuple, Callable, Set import os -import traceback -from contextlib import asynccontextmanager -import sys -import platform import logging -import logging.handlers -import socket -from asyncio import Queue, Lock, Future, Task, Event +from typing import List, Optional, Dict, Any, Set +from asyncio import Queue, Lock, Task, Event from dotenv import load_dotenv load_dotenv() -from fastapi import FastAPI, Request, HTTPException -from fastapi.responses import JSONResponse, StreamingResponse, FileResponse +from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles -from fastapi import WebSocket, WebSocketDisconnect -from pydantic import BaseModel -from playwright.async_api import Page as AsyncPage, Browser as AsyncBrowser, Playwright as AsyncPlaywright, Error as PlaywrightAsyncError, expect as expect_async, BrowserContext as AsyncBrowserContext, Locator, TimeoutError -from playwright.async_api import async_playwright -from urllib.parse import urljoin, urlparse -import uuid -import datetime -import aiohttp -import proxy -import queue +from playwright.async_api import Page as AsyncPage, Browser as AsyncBrowser, Playwright as AsyncPlaywright from config import * -from models import FunctionCall, ToolCall, MessageContentItem, Message, ChatCompletionRequest, ClientDisconnectedError, StreamToLogger, WebSocketConnectionManager, WebSocketLogHandler -from logger import initialize_logging, restore_streams -from browser import _initialize_page_logic, _close_page_logic, signal_camoufox_shutdown, _handle_model_list_response, detect_and_extract_page_error, save_error_snapshot, get_response_via_edit_button, get_response_via_copy_button, _wait_for_response_completion, _get_final_response_content, get_raw_text_content, switch_ai_studio_model, load_excluded_models, _handle_initial_model_state_and_storage, _set_model_from_page_display -from api import generate_sse_chunk, generate_sse_stop_chunk, generate_sse_error_chunk, use_helper_get_response, use_stream_response, clear_stream_queue, prepare_combined_prompt, validate_chat_request, _process_request_refactored, create_app, queue_worker +from models import WebSocketConnectionManager +from api import create_app, queue_worker + STREAM_QUEUE: Optional[multiprocessing.Queue] = None STREAM_PROCESS = None +STREAM_PORT_ACTUAL: Optional[int] = None playwright_manager: Optional[AsyncPlaywright] = None browser_instance: Optional[AsyncBrowser] = None page_instance: Optional[AsyncPage] = None @@ -55,7 +36,7 @@ page_params_cache: Dict[str, Any] = {} params_cache_lock: Optional[Lock] = None logger = logging.getLogger('AIStudioProxyServer') -log_ws_manager = None +log_ws_manager: Optional[WebSocketConnectionManager] = None app = create_app() STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static') @@ -74,4 +55,4 @@ async def read_root(): import uvicorn port = int(os.environ.get('PORT', 2048)) logger.info(f"✨ Dashboard available at: http://localhost:{port}") - uvicorn.run('server:app', host='0.0.0.0', port=port, log_level='info', access_log=False) \ No newline at end of file + uvicorn.run('server:app', host='0.0.0.0', port=port, log_level='info', access_log=False) diff --git a/src/worker/pool.py b/src/worker/pool.py index 74dc25e..66d2539 100644 --- a/src/worker/pool.py +++ b/src/worker/pool.py @@ -1,5 +1,4 @@ import asyncio -import inspect import json import logging import os @@ -155,6 +154,7 @@ def init_from_config(self): logger.info(f"Loaded {len(self.workers)} workers from config") def _dispatch_listener_result(self, result: Any): + import inspect if not inspect.isawaitable(result): return try: