Skip to content
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ uv run playwright install firefox
3. **启动有头模式进行认证**:
- 点击"启动有头模式 (新终端)"
- **命令行终端**内输入`N`,获取新的认证文件
- 命令行终端指`start_webui.bat`启动的终端,或者您运行`uv run python app_launcher.py`的终端
- 命令行终端指`start_webui.bat`启动的终端,或者您运行`uv run python src/app_launcher.py`的终端
- 浏览器会自动打开并导航到 AI Studio
- 手动登录您的 Google 账号
- 确保进入 AI Studio 主页
Expand Down 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
8 changes: 4 additions & 4 deletions docs/api-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

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

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

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

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

# 查看当前密钥(注意安全)
cat key.txt
cat data/key.txt
```

#### 通过 Web UI 管理
Expand Down
25 changes: 3 additions & 22 deletions docs/authentication-setup.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,10 @@
# 首次运行与认证设置指南

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

## 认证文件的重要性

**认证文件是无头模式的关键**: 无头模式依赖于 `auth_profiles/active/` 目录下的有效 `.json` 文件来维持登录状态和访问权限。**文件可能会过期**,需要定期通过 [`launch_camoufox.py --debug`](../launch_camoufox.py) 模式手动运行、登录并保存新的认证文件来替换更新。

## 方法一:通过命令行运行 Debug 模式

**推荐使用 .env 配置方式**:
```env
# .env 文件配置
DEFAULT_FASTAPI_PORT=2048
STREAM_PORT=0
LAUNCH_MODE=normal
DEBUG_LOGS_ENABLED=true
```

```bash
# 简化启动命令 (推荐)
uv run python launch_camoufox.py --debug

# 传统命令行方式 (仍然支持)
uv run python launch_camoufox.py --debug --server-port 2048 --stream-port 0 --helper '' --internal-camoufox-proxy ''
```
**认证文件是无头模式的关键**: 无头模式依赖于 `auth_profiles/active/` 目录下的有效 `.json` 文件来维持登录状态和访问权限。**文件可能会过期**,需要定期通过 [`src/launch_camoufox.py --debug`](../src/launch_camoufox.py) 模式手动运行、登录并保存新的认证文件来替换更新。

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

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

1. 运行 `uv run python app_launcher.py`。
1. 运行 `uv run python src/app_launcher.py`。
2. 浏览器会自动打开管理界面(默认 `http://127.0.0.1:9000`)。
3. 在 `配置` 页面选择 `调试模式 (Debug)`。
4. 点击 `启动服务` 按钮。
Expand Down
6 changes: 3 additions & 3 deletions docs/multi-worker-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ Worker 配置保存在 `data/workers.json`:
```json
{
"workers": [
{"id": "w1", "profile": "account1.json", "port": 3001, "camoufox_port": 9223},
{"id": "w2", "profile": "account2.json", "port": 3002, "camoufox_port": 9224}
{"id": "w1", "profile": "account1.json", "port": 3001, "camoufox_port": 40222},
{"id": "w2", "profile": "account2.json", "port": 3002, "camoufox_port": 40223}
],
"settings": {"recovery_hours": 6}
}
Expand All @@ -85,7 +85,7 @@ Worker 配置保存在 `data/workers.json`:
## 注意事项

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

Expand Down
38 changes: 24 additions & 14 deletions src/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
6 changes: 3 additions & 3 deletions src/api/queue_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}')

Expand All @@ -234,7 +234,7 @@ async def non_streaming_disconnect_monitor():
else:
logger.info(f'[{req_id}] (Worker) 发送按钮已禁用,无需点击。')
except Exception as button_check_err:
logger.warning(f'[{req_id}] (Worker) 检查按钮状态失败: {button_check_err}')
logger.debug(f'[{req_id}] (Worker) 检查按钮状态失败: {button_check_err}')
logger.info(f'[{req_id}] (Worker) 等待发送按钮最终禁用...')
await expect_async(submit_btn_loc).to_be_disabled(timeout=wait_timeout_ms)
logger.info(f'[{req_id}] ✅ 发送按钮已禁用。')
Expand Down
10 changes: 8 additions & 2 deletions src/api/request_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down 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
Loading
Loading