Skip to content

Commit b2d4967

Browse files
authored
Merge pull request #12 from maebahesioru/main
fix: improve stability - timeouts, parameter setting, Camoufox retry, error handling
2 parents 8dacd04 + 9e8f03e commit b2d4967

24 files changed

Lines changed: 303 additions & 195 deletions

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ PORT=2048
1010

1111
# GUI 启动器默认端口配置
1212
DEFAULT_FASTAPI_PORT=2048
13-
DEFAULT_CAMOUFOX_PORT=9222
13+
DEFAULT_CAMOUFOX_PORT=40222
1414

1515
# 流式代理服务配置
1616
STREAM_PORT=3120
@@ -64,7 +64,7 @@ AUTO_CONFIRM_LOGIN=true
6464
# =============================================================================
6565

6666
# Camoufox WebSocket 端点
67-
# CAMOUFOX_WS_ENDPOINT=ws://127.0.0.1:9222
67+
# CAMOUFOX_WS_ENDPOINT=ws://127.0.0.1:40222
6868

6969
# 启动模式 (normal, headless, virtual_display, direct_debug_no_browser)
7070
LAUNCH_MODE=normal

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ uv run playwright install firefox
140140
3. **启动有头模式进行认证**:
141141
- 点击"启动有头模式 (新终端)"
142142
- **命令行终端**内输入`N`,获取新的认证文件
143-
- 命令行终端指`start_webui.bat`启动的终端,或者您运行`uv run python app_launcher.py`的终端
143+
- 命令行终端指`start_webui.bat`启动的终端,或者您运行`uv run python src/app_launcher.py`的终端
144144
- 浏览器会自动打开并导航到 AI Studio
145145
- 手动登录您的 Google 账号
146146
- 确保进入 AI Studio 主页
@@ -350,7 +350,7 @@ cp .env.example .env
350350
### 端口配置
351351

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

356356
## 🔧 高级功能

README_en.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ cp .env.example .env
344344
### Port Configuration
345345

346346
- **FastAPI Service**: Default port `2048`
347-
- **Camoufox Debug**: Default port `9222`
347+
- **Camoufox Debug**: Default port `40222`
348348
- **Streaming Proxy**: Default port `3120`
349349

350350
## 🔧 Advanced Features

docs/api-usage.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
项目使用 `key.txt` 文件来管理API密钥:
2020

21-
**文件位置**: 项目根目录下的 `key.txt` 文件
21+
**文件位置**: `data/key.txt`
2222

2323
**文件格式**: 每行一个API密钥,支持空行和注释
2424
```
@@ -34,13 +34,13 @@ another-api-key
3434
### 密钥管理方法
3535

3636
#### 手动编辑文件
37-
直接编辑 `key.txt` 文件添加或删除密钥:
37+
直接编辑 `data/key.txt` 文件添加或删除密钥:
3838
```bash
3939
# 添加密钥
40-
echo "your-new-api-key" >> key.txt
40+
echo "your-new-api-key" >> data/key.txt
4141

4242
# 查看当前密钥(注意安全)
43-
cat key.txt
43+
cat data/key.txt
4444
```
4545

4646
#### 通过 Web UI 管理

docs/authentication-setup.md

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,10 @@
11
# 首次运行与认证设置指南
22

3-
为了避免每次启动都手动登录 AI Studio,你需要先通过 [`launch_camoufox.py --debug`](../launch_camoufox.py) 模式或 [`app_launcher.py`](../app_launcher.py) 的有头模式运行一次来生成认证文件。
3+
为了避免每次启动都手动登录 AI Studio,你需要先通过 [`src/launch_camoufox.py --debug`](../src/launch_camoufox.py) 模式或 [`src/app_launcher.py`](../src/app_launcher.py) 的有头模式运行一次来生成认证文件。
44

55
## 认证文件的重要性
66

7-
**认证文件是无头模式的关键**: 无头模式依赖于 `auth_profiles/active/` 目录下的有效 `.json` 文件来维持登录状态和访问权限。**文件可能会过期**,需要定期通过 [`launch_camoufox.py --debug`](../launch_camoufox.py) 模式手动运行、登录并保存新的认证文件来替换更新。
8-
9-
## 方法一:通过命令行运行 Debug 模式
10-
11-
**推荐使用 .env 配置方式**:
12-
```env
13-
# .env 文件配置
14-
DEFAULT_FASTAPI_PORT=2048
15-
STREAM_PORT=0
16-
LAUNCH_MODE=normal
17-
DEBUG_LOGS_ENABLED=true
18-
```
19-
20-
```bash
21-
# 简化启动命令 (推荐)
22-
uv run python launch_camoufox.py --debug
23-
24-
# 传统命令行方式 (仍然支持)
25-
uv run python launch_camoufox.py --debug --server-port 2048 --stream-port 0 --helper '' --internal-camoufox-proxy ''
26-
```
7+
**认证文件是无头模式的关键**: 无头模式依赖于 `auth_profiles/active/` 目录下的有效 `.json` 文件来维持登录状态和访问权限。**文件可能会过期**,需要定期通过 [`src/launch_camoufox.py --debug`](../src/launch_camoufox.py) 模式手动运行、登录并保存新的认证文件来替换更新。
278

289
**重要参数说明:**
2910
* `--debug`: 启动有头模式,用于首次认证和调试
@@ -49,7 +30,7 @@ uv run python launch_camoufox.py --debug --server-port 2048 --stream-port 0 --he
4930

5031
## 方法二:通过 GUI 启动有头模式
5132

52-
1. 运行 `uv run python app_launcher.py`
33+
1. 运行 `uv run python src/app_launcher.py`
5334
2. 浏览器会自动打开管理界面(默认 `http://127.0.0.1:9000`)。
5435
3.`配置` 页面选择 `调试模式 (Debug)`
5536
4. 点击 `启动服务` 按钮。

docs/multi-worker-guide.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ Worker 配置保存在 `data/workers.json`:
6969
```json
7070
{
7171
"workers": [
72-
{"id": "w1", "profile": "account1.json", "port": 3001, "camoufox_port": 9223},
73-
{"id": "w2", "profile": "account2.json", "port": 3002, "camoufox_port": 9224}
72+
{"id": "w1", "profile": "account1.json", "port": 3001, "camoufox_port": 40222},
73+
{"id": "w2", "profile": "account2.json", "port": 3002, "camoufox_port": 40223}
7474
],
7575
"settings": {"recovery_hours": 6}
7676
}
@@ -85,7 +85,7 @@ Worker 配置保存在 `data/workers.json`:
8585
## 注意事项
8686

8787
1. **端口分配**: 每个 Worker 自动分配独立端口,避免冲突
88-
2. **流式代理端口**: Worker-w1 使用 3120Worker-w2 使用 3121,以此类推
88+
2. **流式代理端口**: Worker 启动时从 `stream_port`(默认 3120)开始依次递增分配,与 Worker ID 无固定对应关系
8989
3. **账号安全**: 确保每个账号的认证文件独立,不要共用
9090
4. **资源占用**: 每个 Worker 运行独立的浏览器实例,注意内存占用
9191

src/api/app.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from models import WebSocketConnectionManager
1515
from logger import initialize_logging, restore_streams
1616
from browser import _initialize_page_logic, _close_page_logic, load_excluded_models, _handle_initial_model_state_and_storage
17-
import proxy
1817
from asyncio import Queue, Lock
1918
from . import auth_utils
2019
playwright_manager: Optional[AsyncPlaywright] = None
@@ -82,24 +81,30 @@ async def _wait_for_port(port: int, timeout: float = 10.0, interval: float = 0.3
8281
return False
8382

8483
async def _start_stream_proxy():
84+
import proxy
8585
import server
8686
STREAM_PORT = os.environ.get('STREAM_PORT')
8787
if STREAM_PORT != '0':
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
@@ -153,7 +158,7 @@ async def _shutdown_resources():
153158
async def lifespan(app: FastAPI):
154159
"""FastAPI application life cycle management"""
155160
import server
156-
from server import queue_worker
161+
from api import queue_worker
157162
original_streams = (sys.stdout, sys.stderr)
158163
initial_stdout, initial_stderr = _setup_logging()
159164
logger = server.logger
@@ -175,7 +180,12 @@ 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+
elif 'NS_ERROR_PROXY' in str(e) or 'PROXY_CONNECTION_REFUSED' in str(e):
186+
logger.warning(f'Application startup failed (proxy error): {e}')
187+
else:
188+
logger.critical(f'Application startup failed: {e}', exc_info=True)
179189
await _shutdown_resources()
180190
raise RuntimeError(f'Application startup failed: {e}') from e
181191
finally:

src/api/queue_worker.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,11 @@ async def non_streaming_disconnect_monitor():
205205

206206
try:
207207
if completion_event:
208-
from server import RESPONSE_COMPLETION_TIMEOUT
208+
from config import RESPONSE_COMPLETION_TIMEOUT
209209
await asyncio.wait_for(completion_event.wait(), timeout=RESPONSE_COMPLETION_TIMEOUT / 1000 + 60)
210210
logger.info(f'[{req_id}] (Worker) ✅ 流式生成器完成信号收到。客户端提前断开: {client_disconnected_early}')
211211
else:
212-
from server import RESPONSE_COMPLETION_TIMEOUT
212+
from config import RESPONSE_COMPLETION_TIMEOUT
213213
await asyncio.wait_for(asyncio.shield(result_future), timeout=RESPONSE_COMPLETION_TIMEOUT / 1000 + 60)
214214
logger.info(f'[{req_id}] (Worker) ✅ 非流式处理完成。客户端提前断开: {client_disconnected_early}')
215215

@@ -234,7 +234,7 @@ async def non_streaming_disconnect_monitor():
234234
else:
235235
logger.info(f'[{req_id}] (Worker) 发送按钮已禁用,无需点击。')
236236
except Exception as button_check_err:
237-
logger.warning(f'[{req_id}] (Worker) 检查按钮状态失败: {button_check_err}')
237+
logger.debug(f'[{req_id}] (Worker) 检查按钮状态失败: {button_check_err}')
238238
logger.info(f'[{req_id}] (Worker) 等待发送按钮最终禁用...')
239239
await expect_async(submit_btn_loc).to_be_disabled(timeout=wait_timeout_ms)
240240
logger.info(f'[{req_id}] ✅ 发送按钮已禁用。')

src/api/request_processor.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from models import ChatCompletionRequest, ClientDisconnectedError
1616
from browser import switch_ai_studio_model, save_error_snapshot
1717
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
18-
from .abort_detector import AbortSignalDetector, AbortSignalHandler
18+
from .abort_detector import AbortSignalHandler
1919
from browser.page_controller import PageController
2020

2121
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
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: 19 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,33 @@ 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+
is_proxy_error = 'NS_ERROR_PROXY' in str(e_init_page) or 'PROXY_CONNECTION_REFUSED' in str(e_init_page)
352+
if is_browser_closed:
353+
logger.warning(f'页面初始化时浏览器已关闭: {e_init_page}')
354+
elif is_proxy_error:
355+
logger.warning(f'页面初始化时代理连接失败: {e_init_page}')
356+
else:
357+
logger.critical(f'❌ 页面逻辑初始化期间发生严重意外错误: {e_init_page}', exc_info=True)
347358
if temp_context:
348359
try:
349360
logger.info(f' 尝试关闭临时的浏览器上下文 due to initialization error.')
350361
await temp_context.close()
351362
logger.info(' ✅ 临时浏览器上下文已关闭。')
352363
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')
364+
logger.debug(f' 关闭临时浏览器上下文时出错: {close_err}')
365+
if not is_browser_closed:
366+
from .operations import save_error_snapshot
367+
await save_error_snapshot('init_unexpected_error')
356368
raise RuntimeError(f'页面初始化意外错误: {e_init_page}') from e_init_page
357369

358370
async def _close_page_logic():

0 commit comments

Comments
 (0)