Skip to content

Commit b495e9d

Browse files
author
maebahesioru
committed
fix: improve stability - timeouts, parameter setting, Camoufox retry, error handling
Parameter Setting: - Use fill() instead of type() for spinbutton inputs (fixes decimal truncation) - Re-expand advanced settings panel if parameter inputs not visible - scroll_into_view_if_needed for Google Search toggle and stop sequence input - Google Search: JS label click fallback when force click fails Submission: - Submit Button wait timeout: 2s -> 10s - Textarea locator timeout: 5s -> 15s - If shortcut fails but response already started, treat as success Camoufox Launch: - Retry up to 3 times on WebSocket endpoint timeout - Find free port before each attempt to avoid EADDRINUSE - Early exit on EADDRINUSE detection (no need to wait full 90s) Error Handling / Log Noise: - Downgrade TargetClosedError in model management and localStorage init to DEBUG - Downgrade noisy button check warnings to DEBUG - STREAM proxy: retry with port increment on failure Other: - Clear chat verification: fallback to textarea when zero_state not visible - Tools panel / Google Search: skip if button not found (new UI layout) - Fuzzy model ID matching when model not in parsed_model_list - SLEEP_LONG 0.5s -> 1.0s
1 parent 8dacd04 commit b495e9d

8 files changed

Lines changed: 223 additions & 106 deletions

File tree

src/api/app.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,18 +88,23 @@ async def _start_stream_proxy():
8888
port = int(STREAM_PORT or 3120)
8989
STREAM_PROXY_SERVER_ENV = os.environ.get('UNIFIED_PROXY_CONFIG') or os.environ.get('HTTPS_PROXY') or os.environ.get('HTTP_PROXY')
9090
server.logger.info(f'Starting STREAM proxy on port {port} with upstream proxy: {STREAM_PROXY_SERVER_ENV}')
91-
server.STREAM_QUEUE = multiprocessing.Queue()
92-
server.STREAM_PROCESS = multiprocessing.Process(target=proxy.start, args=(server.STREAM_QUEUE, port, STREAM_PROXY_SERVER_ENV))
93-
server.STREAM_PROCESS.start()
94-
server.logger.info('STREAM proxy process started. Waiting for port readiness...')
95-
if await _wait_for_port(port):
96-
server.logger.info(f'STREAM proxy port {port} is ready.')
97-
else:
98-
server.logger.error(f'STREAM proxy port {port} not ready after timeout. Browser may fail to connect.')
99-
if server.STREAM_PROCESS and server.STREAM_PROCESS.is_alive():
100-
server.logger.warning('STREAM proxy process is alive but port not listening.')
91+
for attempt in range(3):
92+
current_port = port + attempt
93+
server.STREAM_QUEUE = multiprocessing.Queue()
94+
server.STREAM_PROCESS = multiprocessing.Process(target=proxy.start, args=(server.STREAM_QUEUE, current_port, STREAM_PROXY_SERVER_ENV))
95+
server.STREAM_PROCESS.start()
96+
server.logger.info(f'STREAM proxy process started on port {current_port}. Waiting for port readiness...')
97+
if await _wait_for_port(current_port, timeout=30.0):
98+
server.STREAM_PORT_ACTUAL = current_port
99+
server.logger.info(f'STREAM proxy port {current_port} is ready.')
100+
if current_port != port:
101+
server.logger.warning(f'STREAM proxy using fallback port {current_port} (requested {port}).')
102+
return
101103
else:
102-
server.logger.error(f'STREAM proxy process died. Exit code: {server.STREAM_PROCESS.exitcode}')
104+
server.logger.warning(f'STREAM proxy port {current_port} not ready, killing process...')
105+
server.STREAM_PROCESS.terminate()
106+
server.STREAM_PROCESS.join(timeout=3)
107+
server.logger.error(f'STREAM proxy failed to start after 3 attempts.')
103108

104109
async def _initialize_browser_and_page():
105110
import server
@@ -175,7 +180,10 @@ async def lifespan(app: FastAPI):
175180
server.is_initializing = False
176181
yield
177182
except Exception as e:
178-
logger.critical(f'Application startup failed: {e}', exc_info=True)
183+
if 'Target page, context or browser has been closed' in str(e):
184+
logger.warning(f'Application startup failed (browser closed): {e}')
185+
else:
186+
logger.critical(f'Application startup failed: {e}', exc_info=True)
179187
await _shutdown_resources()
180188
raise RuntimeError(f'Application startup failed: {e}') from e
181189
finally:

src/api/request_processor.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,13 @@ async def _analyze_model_requirements(req_id: str, context: dict, request: ChatC
114114
if parsed_model_list:
115115
valid_model_ids = [m.get('id') for m in parsed_model_list]
116116
if requested_model_id not in valid_model_ids:
117-
raise HTTPException(status_code=400, detail=f"[{req_id}] Invalid model '{requested_model_id}'. Available models: {', '.join(valid_model_ids)}")
117+
# fuzzy match: find model whose id contains the requested id or vice versa
118+
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)
119+
if fuzzy:
120+
logger.info(f'[{req_id}] 模型 "{requested_model_id}" 不在列表中,自动映射到 "{fuzzy}"')
121+
requested_model_id = fuzzy
122+
else:
123+
raise HTTPException(status_code=400, detail=f"[{req_id}] Invalid model '{requested_model_id}'. Available models: {', '.join(valid_model_ids)}")
118124
context['model_id_to_use'] = requested_model_id
119125
if current_ai_studio_model_id != requested_model_id:
120126
context['needs_model_switching'] = True

src/browser/initialization.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -312,12 +312,12 @@ async def _initialize_page_logic(browser: AsyncBrowser):
312312
if wrapper_locator:
313313
logger.info(f'✅ 输入框wrapper可见 (匹配: {wrapper_matched})')
314314
else:
315-
logger.warning('⚠️ 未找到任何wrapper,尝试直接查找输入框')
316-
input_locator, matched = await wait_for_any_selector(found_page, PROMPT_TEXTAREA_SELECTORS, timeout=10000)
315+
logger.debug('⚠️ 未找到任何wrapper,尝试直接查找输入框')
316+
input_locator, matched = await wait_for_any_selector(found_page, PROMPT_TEXTAREA_SELECTORS, timeout=30000)
317317
if input_locator:
318318
logger.info(f'✅ 核心输入区域可见 (匹配: {matched})')
319319
else:
320-
await expect_async(found_page.locator(INPUT_SELECTOR)).to_be_visible(timeout=10000)
320+
await expect_async(found_page.locator(INPUT_SELECTOR)).to_be_visible(timeout=30000)
321321
logger.info('✅ 核心输入区域可见 (默认选择器)')
322322
try:
323323
from config.selectors import MODEL_SELECTORS_LIST
@@ -338,21 +338,30 @@ async def _initialize_page_logic(browser: AsyncBrowser):
338338
logger.info(f'✅ 页面逻辑初始化成功。')
339339
return (result_page_instance, result_page_ready)
340340
except Exception as input_visible_err:
341+
from playwright._impl._errors import TargetClosedError
342+
if isinstance(input_visible_err, TargetClosedError) or 'Target page, context or browser has been closed' in str(input_visible_err):
343+
logger.warning(f'页面初始化时浏览器已关闭,跳过。')
344+
raise
341345
from .operations import save_error_snapshot
342346
await save_error_snapshot('init_fail_input_timeout')
343347
logger.error(f'页面初始化失败:核心输入区域未在预期时间内变为可见。最后的 URL 是 {found_page.url}', exc_info=True)
344348
raise RuntimeError(f'页面初始化失败:核心输入区域未在预期时间内变为可见。最后的 URL 是 {found_page.url}') from input_visible_err
345349
except Exception as e_init_page:
346-
logger.critical(f'❌ 页面逻辑初始化期间发生严重意外错误: {e_init_page}', exc_info=True)
350+
is_browser_closed = 'Target page, context or browser has been closed' in str(e_init_page)
351+
if is_browser_closed:
352+
logger.warning(f'页面初始化时浏览器已关闭: {e_init_page}')
353+
else:
354+
logger.critical(f'❌ 页面逻辑初始化期间发生严重意外错误: {e_init_page}', exc_info=True)
347355
if temp_context:
348356
try:
349357
logger.info(f' 尝试关闭临时的浏览器上下文 due to initialization error.')
350358
await temp_context.close()
351359
logger.info(' ✅ 临时浏览器上下文已关闭。')
352360
except Exception as close_err:
353-
logger.warning(f' ⚠️ 关闭临时浏览器上下文时出错: {close_err}')
354-
from .operations import save_error_snapshot
355-
await save_error_snapshot('init_unexpected_error')
361+
logger.debug(f' 关闭临时浏览器上下文时出错: {close_err}')
362+
if not is_browser_closed:
363+
from .operations import save_error_snapshot
364+
await save_error_snapshot('init_unexpected_error')
356365
raise RuntimeError(f'页面初始化意外错误: {e_init_page}') from e_init_page
357366

358367
async def _close_page_logic():

src/browser/model_management.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ async def _verify_ui_state_settings(page: AsyncPage, req_id: str='unknown') -> d
2929
logger.error(f'[{req_id}] ❌ 解析localStorage JSON失败: {e}')
3030
return {'exists': False, 'isAdvancedOpen': None, 'areToolsOpen': None, 'needsUpdate': True, 'error': f'JSON解析失败: {e}'}
3131
except Exception as e:
32-
logger.error(f'[{req_id}] ❌ 验证UI状态设置时发生错误: {e}')
32+
if 'Target page, context or browser has been closed' in str(e):
33+
logger.debug(f'[{req_id}] UI状态验证时浏览器已关闭')
34+
else:
35+
logger.error(f'[{req_id}] ❌ 验证UI状态设置时发生错误: {e}')
3336
return {'exists': False, 'isAdvancedOpen': None, 'areToolsOpen': None, 'needsUpdate': True, 'error': f'验证失败: {e}'}
3437

3538
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
8487
return False
8588

8689
except Exception as e:
87-
logger.error(f'[{req_id}] ❌ 强制设置UI状态错误: {e}')
90+
if 'Target page, context or browser has been closed' in str(e):
91+
logger.debug(f'[{req_id}] 强制设置UI时浏览器已关闭')
92+
else:
93+
logger.error(f'[{req_id}] ❌ 强制设置UI状态错误: {e}')
8894
return False
8995

9096
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):
385391
except Exception as reload_err:
386392
err_str = str(reload_err)
387393
if 'Target page, context or browser has been closed' in err_str or 'Browser has been closed' in err_str:
388-
logger.warning(f' ⚠️ 浏览器已关闭,跳过重新加载。')
394+
logger.debug(f' ⚠️ 浏览器已关闭,跳过重新加载。')
389395
return
390396
logger.warning(f' ⚠️ 页面重新加载尝试 {attempt + 1}/{max_retries} 失败: {reload_err}')
391397
if attempt < max_retries - 1:
@@ -401,6 +407,10 @@ async def _handle_initial_model_state_and_storage(page: AsyncPage):
401407
else:
402408
logger.info(' localStorage 状态良好 (isAdvancedOpen=true, promptModel有效),无需刷新页面。')
403409
except Exception as e:
410+
from playwright._impl._errors import TargetClosedError
411+
if isinstance(e, TargetClosedError):
412+
logger.debug(f'处理初始模型状态时浏览器已关闭: {e}')
413+
return
404414
logger.error(f'❌ (新) 处理初始模型状态和 localStorage 时发生严重错误: {e}', exc_info=True)
405415
try:
406416
logger.warning(' 由于发生错误,尝试回退仅从页面显示设置全局模型 ID (不写入localStorage)...')
@@ -421,7 +431,7 @@ async def _set_model_from_page_display(page: AsyncPage, set_storage: bool=False)
421431
)
422432

423433
if not displayed_model_name:
424-
logger.warning(' 所有选择器都无法获取页面显示的模型名称')
434+
logger.debug(' 所有选择器都无法获取页面显示的模型名称')
425435
displayed_model_name = '未知模型'
426436
found_model_id_from_display = None
427437
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)
437447
logger.info(f" 显示名称 '{displayed_model_name}' 对应模型 ID: {found_model_id_from_display}")
438448
break
439449
if not found_model_id_from_display:
440-
logger.warning(f" 未在已知模型列表中找到与显示名称 '{displayed_model_name}' 匹配的 ID。")
450+
logger.debug(f" 未在已知模型列表中找到与显示名称 '{displayed_model_name}' 匹配的 ID。")
441451
else:
442452
logger.warning(' 模型列表尚不可用,无法将显示名称转换为ID。')
443453
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)
478488
await page.evaluate("(prefsStr) => localStorage.setItem('aiStudioUserPreference', prefsStr)", json.dumps(prefs_to_set))
479489
logger.info(f" ✅ localStorage.aiStudioUserPreference 已更新。isAdvancedOpen: {prefs_to_set.get('isAdvancedOpen')}, areToolsOpen: {prefs_to_set.get('areToolsOpen')} (期望: True), promptModel: '{prefs_to_set.get('promptModel', '未设置/保留原样')}'。")
480490
except Exception as e_set_disp:
481-
logger.error(f' 尝试从页面显示设置模型时出错: {e_set_disp}', exc_info=True)
491+
from playwright._impl._errors import TargetClosedError
492+
if isinstance(e_set_disp, TargetClosedError):
493+
logger.debug(f' 尝试从页面显示设置模型时出错 (browser closed): {e_set_disp}')
494+
else:
495+
logger.error(f' 尝试从页面显示设置模型时出错: {e_set_disp}', exc_info=True)

0 commit comments

Comments
 (0)