Skip to content

Commit 4a977bb

Browse files
committed
refactor: split large modules, enforce strict typing, and expand test suite
1 parent f7349a4 commit 4a977bb

98 files changed

Lines changed: 10978 additions & 4599 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ This project is generously sponsored by ZMTO. Visit their website: [https://zmto
3636
| **内存** | ≥2GB | ≥4GB | 浏览器自动化需要 |
3737
| **网络** | 稳定互联网连接 | - | 可配置代理访问 Google AI Studio |
3838
| **依赖管理** | Poetry | 最新版本 | 现代化 Python 依赖管理工具 |
39-
| **类型检查** | Pyright (可选) | 最新版本 | 用于开发时类型检查和 IDE 支持 |
39+
| **类型检查** | Pyright (可选) | 最新版本 | 用于开发时类型检查和 IDE 支持 (需要 Node.js) |
4040

4141

4242
## 系统要求
4343

4444
- **Python**: >=3.9, <4.0 (推荐 3.10+ 以获得最佳性能,Docker 环境使用 3.10)
4545
- **依赖管理**: [Poetry](https://python-poetry.org/) (现代化 Python 依赖管理工具,替代传统 requirements.txt)
46-
- **类型检查**: [Pyright](https://github.com/microsoft/pyright) (可选,用于开发时类型检查和 IDE 支持)
46+
- **类型检查**: [Pyright](https://github.com/microsoft/pyright) (可选,用于开发时类型检查和 IDE 支持,需要 Node.js 环境)
4747
- **操作系统**: Windows, macOS, Linux (完全跨平台支持,Docker 部署支持 x86_64 和 ARM64)
4848
- **内存**: 建议 2GB+ 可用内存 (浏览器自动化需要)
4949
- **网络**: 稳定的互联网连接访问 Google AI Studio (支持代理配置)
@@ -55,7 +55,7 @@ This project is generously sponsored by ZMTO. Visit their website: [https://zmto
5555
- **智能模型切换**: 通过 API 请求中的 `model` 字段动态切换 AI Studio 中的模型
5656
- **完整参数控制**: 支持 `temperature``max_output_tokens``top_p``stop``reasoning_effort` 等所有主要参数
5757
- **反指纹检测**: 使用 Camoufox 浏览器降低被检测为自动化脚本的风险
58-
- **脚本注入功能 v3.0**: 使用 Playwright 原生网络拦截,支持油猴脚本动态挂载,100%可靠 🆕
58+
- **脚本注入功能 v3.0**: 使用 Playwright 原生网络拦截,支持油猴脚本动态挂载,更加稳定
5959
- **现代化 Web UI**: 内置测试界面,支持实时聊天、状态监控、分级 API 密钥管理
6060
- **图形界面启动器**: 提供功能丰富的 GUI 启动器,简化配置和进程管理
6161
- **灵活认证系统**: 支持可选的 API 密钥认证,完全兼容 OpenAI 标准的 Bearer token 格式
@@ -87,7 +87,8 @@ graph TD
8787
RequestProcessor["api_utils/request_processor.py (请求处理)"]
8888
AuthUtils["api_utils/auth_utils.py (认证管理)"]
8989
PageController["browser_utils/page_controller.py (页面控制)"]
90-
ScriptManager["browser_utils/script_manager.py (脚本注入)"]
90+
BrowserInit["browser_utils/initialization/ (初始化模块)"]
91+
BrowserOps["browser_utils/operations_modules/ (操作模块)"]
9192
ModelManager["browser_utils/model_management.py (模型管理)"]
9293
StreamProxy["stream/ (流式代理服务器)"]
9394
end
@@ -119,10 +120,11 @@ graph TD
119120
RequestProcessor -- "控制浏览器 (Controls Browser)" --> PageController
120121
RequestProcessor -- "通过代理 (Uses Proxy)" --> StreamProxy
121122
123+
PageController -- "初始化 (Initializes)" --> BrowserInit
124+
PageController -- "执行操作 (Executes Ops)" --> BrowserOps
122125
PageController -- "模型管理 (Model Management)" --> ModelManager
123-
PageController -- "脚本注入 (Script Injection)" --> ScriptManager
124-
ScriptManager -- "加载脚本 (Loads Script)" --> UserScript
125-
ScriptManager -- "增强功能 (Enhances)" --> CamoufoxInstance
126+
BrowserInit -- "脚本注入 (Script Injection)" --> UserScript
127+
BrowserInit -- "增强功能 (Enhances)" --> CamoufoxInstance
126128
PageController -- "自动化 (Automates)" --> CamoufoxInstance
127129
CamoufoxInstance -- "访问 (Accesses)" --> AI_Studio
128130
StreamProxy -- "转发请求 (Forwards Request)" --> AI_Studio
@@ -171,12 +173,12 @@ curl http://127.0.0.1:2048/v1/models
171173
# 测试聊天(非流式)
172174
curl -X POST http://127.0.0.1:2048/v1/chat/completions \
173175
-H "Content-Type: application/json" \
174-
-d '{"model":"gemini-2.5-pro","messages":[{"role":"user","content":"Hello"}]}'
176+
-d '{"model":"gemini-1.5-pro","messages":[{"role":"user","content":"Hello"}]}'
175177

176178
# 测试流式聊天
177179
curl -X POST http://127.0.0.1:2048/v1/chat/completions \
178180
-H "Content-Type: application/json" \
179-
-d '{"model":"gemini-2.5-pro","messages":[{"role":"user","content":"讲个故事"}],"stream":true}' --no-buffer
181+
-d '{"model":"gemini-1.5-pro","messages":[{"role":"user","content":"讲个故事"}],"stream":true}' --no-buffer
180182
```
181183

182184
### 访问 Web UI
@@ -369,11 +371,11 @@ nano .env # 或使用其他编辑器
369371
- **[OpenAI 兼容性说明](docs/openai-compatibility.md)** - 与 OpenAI API 的差异和限制 🔄
370372
- **[客户端集成示例](docs/client-examples.md)** - Python、JavaScript、cURL 等示例代码 💻
371373
- [Web UI 使用指南](docs/webui-guide.md) - Web 界面功能说明
372-
- [脚本注入指南](docs/script_injection_guide.md) - 油猴脚本动态挂载功能使用指南 (v3.0) 🆕
374+
- [脚本注入指南](docs/script_injection_guide.md) - 油猴脚本动态挂载功能使用指南 (v3.0)
373375

374376
#### ⚙️ 高级配置
375377

376-
- [流式处理模式详解](docs/streaming-modes.md) - 三层响应获取机制详细说明 🆕
378+
- [流式处理模式详解](docs/streaming-modes.md) - 三层响应获取机制详细说明
377379
- [高级配置指南](docs/advanced-configuration.md) - 高级功能和配置选项
378380
- [日志控制指南](docs/logging-control.md) - 日志系统配置和调试
379381
- [故障排除指南](docs/troubleshooting.md) - 常见问题解决方案
@@ -386,7 +388,7 @@ nano .env # 或使用其他编辑器
386388

387389
#### 🛠️ 开发相关
388390

389-
- [项目架构指南](docs/architecture-guide.md) - 模块化架构设计和组件详解 🆕
391+
- [项目架构指南](docs/architecture-guide.md) - 模块化架构设计和组件详解
390392
- [开发者指南](docs/development-guide.md) - Poetry、Pyright 和开发工作流程
391393
- [依赖版本说明](docs/dependency-versions.md) - Poetry 依赖管理和版本控制详解
392394

api_utils/client_connection.py

Lines changed: 89 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,77 @@
11
import asyncio
2-
from typing import Callable, Tuple
3-
from asyncio import Event
2+
from typing import Callable, Tuple, Optional
3+
from asyncio import Event, Task
44
from fastapi import HTTPException, Request
5+
from models import ClientDisconnectedError
56

6-
7-
async def test_client_connection(req_id: str, http_request: Request) -> bool:
7+
async def check_client_connection(req_id: str, http_request: Request) -> bool:
8+
"""
9+
Checks if the client is still connected.
10+
Returns True if connected, False if disconnected.
11+
"""
812
try:
913
if hasattr(http_request, '_receive'):
1014
try:
11-
receive_task = asyncio.create_task(http_request._receive())
15+
# Use a very short timeout to check for disconnect message
16+
# Cast to Coroutine to satisfy mypy, as _receive is awaitable
17+
from typing import Coroutine, Any
18+
receive_coro: Coroutine[Any, Any, Any] = http_request._receive() # type: ignore
19+
receive_task: Task = asyncio.create_task(receive_coro)
1220
done, pending = await asyncio.wait([receive_task], timeout=0.01)
21+
1322
if done:
1423
message = receive_task.result()
1524
if message.get("type") == "http.disconnect":
1625
return False
1726
else:
27+
# Cancel the task if it didn't complete immediately
1828
receive_task.cancel()
1929
try:
2030
await receive_task
2131
except asyncio.CancelledError:
2232
pass
2333
except Exception:
34+
# If checking fails, assume disconnected to be safe, or log and continue?
35+
# Usually if _receive fails it might mean connection issues.
2436
return False
37+
38+
# Fallback to is_disconnected() if available (Starlette/FastAPI)
39+
if await http_request.is_disconnected():
40+
return False
41+
2542
return True
2643
except Exception:
2744
return False
2845

2946

30-
async def setup_disconnect_monitoring(req_id: str, http_request: Request, result_future) -> Tuple[Event, asyncio.Task, Callable]:
47+
async def setup_disconnect_monitoring(req_id: str, http_request: Request, result_future) -> Tuple[Event, Task, Callable]:
48+
"""
49+
Sets up a background task to monitor client disconnection.
50+
Returns:
51+
- client_disconnected_event: Event set when disconnect is detected
52+
- disconnect_check_task: The background task
53+
- check_client_disconnected: Helper function to raise error if disconnected
54+
"""
3155
from server import logger
3256
client_disconnected_event = Event()
3357

3458
async def check_disconnect_periodically():
3559
while not client_disconnected_event.is_set():
3660
try:
37-
is_connected = await test_client_connection(req_id, http_request)
61+
is_connected = await check_client_connection(req_id, http_request)
3862
if not is_connected:
39-
logger.info(f"[{req_id}] 主动检测到客户端断开连接。")
40-
client_disconnected_event.set()
41-
if not result_future.done():
42-
result_future.set_exception(HTTPException(status_code=499, detail=f"[{req_id}] 客户端关闭了请求"))
43-
break
44-
45-
if await http_request.is_disconnected():
46-
logger.info(f"[{req_id}] 备用检测到客户端断开连接。")
63+
logger.info(f"[{req_id}] Active disconnect check detected client disconnection.")
4764
client_disconnected_event.set()
4865
if not result_future.done():
49-
result_future.set_exception(HTTPException(status_code=499, detail=f"[{req_id}] 客户端关闭了请求"))
66+
result_future.set_exception(HTTPException(status_code=499, detail=f"[{req_id}] Client closed request"))
5067
break
68+
5169
await asyncio.sleep(0.3)
5270
except asyncio.CancelledError:
71+
# Task cancelled, exit gracefully
5372
break
5473
except Exception as e:
55-
logger.error(f"[{req_id}] (Disco Check Task) 错误: {e}")
74+
logger.error(f"[{req_id}] (Disco Check Task) Error: {e}")
5675
client_disconnected_event.set()
5776
if not result_future.done():
5877
result_future.set_exception(HTTPException(status_code=500, detail=f"[{req_id}] Internal disconnect checker error: {e}"))
@@ -62,10 +81,61 @@ async def check_disconnect_periodically():
6281

6382
def check_client_disconnected(stage: str = ""):
6483
if client_disconnected_event.is_set():
65-
logger.info(f"[{req_id}] 在 '{stage}' 检测到客户端断开连接。")
66-
from models import ClientDisconnectedError
84+
logger.info(f"[{req_id}] Client disconnected detected at stage: '{stage}'")
6785
raise ClientDisconnectedError(f"[{req_id}] Client disconnected at stage: {stage}")
6886
return False
6987

7088
return client_disconnected_event, disconnect_check_task, check_client_disconnected
7189

90+
91+
async def enhanced_disconnect_monitor(req_id: str, http_request: Request, completion_event: Event, logger) -> bool:
92+
"""
93+
Enhanced disconnect monitor for streaming responses.
94+
Returns True if client disconnected early.
95+
"""
96+
client_disconnected_early = False
97+
while not completion_event.is_set():
98+
try:
99+
is_connected = await check_client_connection(req_id, http_request)
100+
if not is_connected:
101+
logger.info(f"[{req_id}] (Monitor) ✅ Client disconnected during streaming, triggering completion event.")
102+
client_disconnected_early = True
103+
if not completion_event.is_set():
104+
completion_event.set()
105+
break
106+
await asyncio.sleep(0.3)
107+
except asyncio.CancelledError:
108+
break
109+
except Exception as e:
110+
logger.error(f"[{req_id}] (Monitor) Enhanced disconnect checker error: {e}")
111+
break
112+
return client_disconnected_early
113+
114+
115+
async def non_streaming_disconnect_monitor(req_id: str, http_request: Request, result_future: asyncio.Future, logger) -> bool:
116+
"""
117+
Disconnect monitor for non-streaming responses.
118+
Returns True if client disconnected early.
119+
"""
120+
client_disconnected_early = False
121+
while not result_future.done():
122+
try:
123+
is_connected = await check_client_connection(req_id, http_request)
124+
if not is_connected:
125+
logger.info(f"[{req_id}] (Monitor) ✅ Client disconnected during non-streaming processing.")
126+
client_disconnected_early = True
127+
if not result_future.done():
128+
result_future.set_exception(
129+
HTTPException(
130+
status_code=499,
131+
detail=f"[{req_id}] Client disconnected during processing",
132+
)
133+
)
134+
break
135+
await asyncio.sleep(0.3)
136+
except asyncio.CancelledError:
137+
break
138+
except Exception as e:
139+
logger.error(f"[{req_id}] (Monitor) Non-streaming disconnect checker error: {e}")
140+
break
141+
return client_disconnected_early

0 commit comments

Comments
 (0)