Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ PORT=2048

# GUI 启动器默认端口配置
DEFAULT_FASTAPI_PORT=2048
DEFAULT_CAMOUFOX_PORT=9222
DEFAULT_CAMOUFOX_PORT=40222

# 流式代理服务配置
STREAM_PORT=3120
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ cp .env.example .env
### 端口配置

- **FastAPI 服务**: 默认端口 `2048`
- **Camoufox 调试**: 默认端口 `9222`
- **Camoufox 调试**: 默认端口 `40222`
- **流式代理**: 默认端口 `3120`

## 🔧 高级功能
Expand Down
2 changes: 1 addition & 1 deletion README_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/multi-worker-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
34 changes: 22 additions & 12 deletions src/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,23 @@ async def _start_stream_proxy():
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
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 7 additions & 1 deletion src/api/request_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 19 additions & 7 deletions src/browser/initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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():
Expand Down
26 changes: 20 additions & 6 deletions src/browser/model_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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)...')
Expand All @@ -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()):
Expand All @@ -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
Expand Down Expand Up @@ -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)
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)
Loading
Loading