Spaces:
Running
Running
nacho commited on
Commit ·
f6a18fc
0
Parent(s):
feat: DS2API Browser - CloakBrowser-based DeepSeek API proxy
Browse files- OpenAI/Claude/Gemini/Ollama compatible API endpoints
- Streaming support (SSE format)
- Multi-account pool with acquire/release
- Web-based account import UI
- Browser automation with CloakBrowser (anti-detection)
- Persistent browser profiles
- Human behavior simulation
- .gitignore +17 -0
- README.md +138 -0
- account_manager.py +121 -0
- config.py +70 -0
- deepseek_browser.py +307 -0
- main.py +476 -0
- requirements.txt +5 -0
- run.py +10 -0
- start.py +19 -0
- test.html +292 -0
.gitignore
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 浏览器 profile 数据
|
| 2 |
+
test-profile*/
|
| 3 |
+
profiles/
|
| 4 |
+
*.log
|
| 5 |
+
|
| 6 |
+
# Python
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.py[cod]
|
| 9 |
+
venv/
|
| 10 |
+
.env
|
| 11 |
+
|
| 12 |
+
# 账号数据(敏感信息)
|
| 13 |
+
accounts.json
|
| 14 |
+
|
| 15 |
+
# 系统文件
|
| 16 |
+
.DS_Store
|
| 17 |
+
Thumbs.db
|
README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DS2API Browser
|
| 2 |
+
|
| 3 |
+
基于 CloakBrowser/Playwright 的 DeepSeek API 代理服务。
|
| 4 |
+
|
| 5 |
+
## 特性
|
| 6 |
+
|
| 7 |
+
- **浏览器自动化** - 使用真实浏览器访问 DeepSeek,无法被检测
|
| 8 |
+
- **OpenAI 兼容 API** - 支持 `/v1/chat/completions` 接口
|
| 9 |
+
- **流式响应** - 支持 SSE 流式输出
|
| 10 |
+
- **账号池管理** - 支持多账号轮询
|
| 11 |
+
- **人类行为模拟** - 模拟真实用户操作
|
| 12 |
+
|
| 13 |
+
## 安装
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
cd /home/huanx/code/ds2api-browser
|
| 17 |
+
python -m venv venv
|
| 18 |
+
source venv/bin/activate
|
| 19 |
+
pip install -r requirements.txt
|
| 20 |
+
|
| 21 |
+
# 安装 Playwright 浏览器(如果不用 CloakBrowser)
|
| 22 |
+
playwright install chromium
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
## 使用
|
| 26 |
+
|
| 27 |
+
### 方式 1: 环境变量配置
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
export DS2API_ACCOUNTS="email1@gmail.com:password1;email2@gmail.com:password2"
|
| 31 |
+
export DS2API_KEYS="sk-key1,sk-key2"
|
| 32 |
+
export DS2API_ADMIN_KEY="your-admin-key"
|
| 33 |
+
export DS2API_PORT="5001"
|
| 34 |
+
export DS2API_HEADLESS="true"
|
| 35 |
+
export DS2API_HUMANIZE="true"
|
| 36 |
+
|
| 37 |
+
python main.py
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
### 方式 2: 直接运行
|
| 41 |
+
|
| 42 |
+
```bash
|
| 43 |
+
python start.py
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### 方式 3: 后台运行
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
nohup python main.py > /tmp/ds2api-browser.log 2>&1 &
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## API 使用
|
| 53 |
+
|
| 54 |
+
### 聊天补全
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
curl http://localhost:5001/v1/chat/completions \
|
| 58 |
+
-H "Authorization: Bearer sk-test123456" \
|
| 59 |
+
-H "Content-Type: application/json" \
|
| 60 |
+
-d '{
|
| 61 |
+
"model": "deepseek-chat",
|
| 62 |
+
"messages": [{"role": "user", "content": "Hello!"}]
|
| 63 |
+
}'
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### 流式响应
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
curl http://localhost:5001/v1/chat/completions \
|
| 70 |
+
-H "Authorization: Bearer sk-test123456" \
|
| 71 |
+
-H "Content-Type: application/json" \
|
| 72 |
+
-d '{
|
| 73 |
+
"model": "deepseek-chat",
|
| 74 |
+
"messages": [{"role": "user", "content": "Hello!"}],
|
| 75 |
+
"stream": true
|
| 76 |
+
}'
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
### Python OpenAI SDK
|
| 80 |
+
|
| 81 |
+
```python
|
| 82 |
+
from openai import OpenAI
|
| 83 |
+
|
| 84 |
+
client = OpenAI(
|
| 85 |
+
base_url="http://localhost:5001/v1",
|
| 86 |
+
api_key="sk-test123456"
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
response = client.chat.completions.create(
|
| 90 |
+
model="deepseek-chat",
|
| 91 |
+
messages=[{"role": "user", "content": "Hello!"}]
|
| 92 |
+
)
|
| 93 |
+
print(response.choices[0].message.content)
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
## 健康检查
|
| 97 |
+
|
| 98 |
+
```bash
|
| 99 |
+
# 健康检查
|
| 100 |
+
curl http://localhost:5001/healthz
|
| 101 |
+
|
| 102 |
+
# 就绪检查
|
| 103 |
+
curl http://localhost:5001/readyz
|
| 104 |
+
|
| 105 |
+
# 管理统计
|
| 106 |
+
curl http://localhost:5001/admin/stats -H "admin-key: admin"
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
## 与原版 DS2API 的区别
|
| 110 |
+
|
| 111 |
+
| 特性 | 原版 DS2API | DS2API Browser |
|
| 112 |
+
|------|-------------|----------------|
|
| 113 |
+
| 实现方式 | HTTP 客户端 | 浏览器自动化 |
|
| 114 |
+
| 指纹检测 | 容易被检测 | 无法被检测 |
|
| 115 |
+
| 账号封禁 | 高风险 | 低风险 |
|
| 116 |
+
| 性能 | 快 | 较慢 |
|
| 117 |
+
| 资源占用 | 低 | 高 |
|
| 118 |
+
|
| 119 |
+
## 注意事项
|
| 120 |
+
|
| 121 |
+
1. **首次运行** - Playwright/CloakBrowser 会下载浏览器二进制文件(~200MB)
|
| 122 |
+
2. **资源占用** - 每个浏览器实例占用约 200-500MB 内存
|
| 123 |
+
3. **性能** - 浏览器自动化比直接 HTTP 慢,但更安全
|
| 124 |
+
4. **账号安全** - 建议使用小号测试,不要用主账号
|
| 125 |
+
|
| 126 |
+
## 文件结构
|
| 127 |
+
|
| 128 |
+
```
|
| 129 |
+
ds2api-browser/
|
| 130 |
+
├── main.py # FastAPI 服务器
|
| 131 |
+
├── deepseek_browser.py # 浏览器自动化核心
|
| 132 |
+
├── account_manager.py # 账号池管理
|
| 133 |
+
├── config.py # 配置管理
|
| 134 |
+
├── start.py # 快速启动脚本
|
| 135 |
+
├── run.py # 运行入口
|
| 136 |
+
├── requirements.txt # 依赖列表
|
| 137 |
+
└── README.md # 本文档
|
| 138 |
+
```
|
account_manager.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from collections import deque
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from typing import Dict, Optional
|
| 5 |
+
|
| 6 |
+
from deepseek_browser import DeepSeekBrowser
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class Account:
|
| 11 |
+
email: str
|
| 12 |
+
password: str
|
| 13 |
+
name: str = ""
|
| 14 |
+
proxy: Optional[str] = None
|
| 15 |
+
browser: Optional[DeepSeekBrowser] = field(default=None, repr=False)
|
| 16 |
+
in_use: bool = False
|
| 17 |
+
error_count: int = 0
|
| 18 |
+
logged_in: bool = False
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class AccountManager:
|
| 22 |
+
def __init__(self, max_inflight: int = 1):
|
| 23 |
+
self.accounts: Dict[str, Account] = {}
|
| 24 |
+
self.queue: deque = deque()
|
| 25 |
+
self.max_inflight = max_inflight
|
| 26 |
+
self._lock = asyncio.Lock()
|
| 27 |
+
|
| 28 |
+
def add_account(self, email: str, password: str, name: str = "", proxy: Optional[str] = None):
|
| 29 |
+
self.accounts[email] = Account(
|
| 30 |
+
email=email,
|
| 31 |
+
password=password,
|
| 32 |
+
name=name,
|
| 33 |
+
proxy=proxy,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
async def acquire(self) -> Account:
|
| 37 |
+
async with self._lock:
|
| 38 |
+
for account in self.accounts.values():
|
| 39 |
+
if not account.in_use and account.error_count < 3:
|
| 40 |
+
account.in_use = True
|
| 41 |
+
return account
|
| 42 |
+
|
| 43 |
+
return await self._wait_for_account()
|
| 44 |
+
|
| 45 |
+
async def _wait_for_account(self) -> Account:
|
| 46 |
+
event = asyncio.Event()
|
| 47 |
+
async with self._lock:
|
| 48 |
+
self.queue.append(event)
|
| 49 |
+
|
| 50 |
+
await event.wait()
|
| 51 |
+
|
| 52 |
+
async with self._lock:
|
| 53 |
+
for account in self.accounts.values():
|
| 54 |
+
if not account.in_use and account.error_count < 3:
|
| 55 |
+
account.in_use = True
|
| 56 |
+
return account
|
| 57 |
+
|
| 58 |
+
raise RuntimeError("No account available")
|
| 59 |
+
|
| 60 |
+
async def release(self, account: Account):
|
| 61 |
+
async with self._lock:
|
| 62 |
+
account.in_use = False
|
| 63 |
+
if self.queue:
|
| 64 |
+
event = self.queue.popleft()
|
| 65 |
+
event.set()
|
| 66 |
+
|
| 67 |
+
async def mark_error(self, account: Account):
|
| 68 |
+
async with self._lock:
|
| 69 |
+
account.error_count += 1
|
| 70 |
+
account.in_use = False
|
| 71 |
+
if self.queue:
|
| 72 |
+
event = self.queue.popleft()
|
| 73 |
+
event.set()
|
| 74 |
+
|
| 75 |
+
async def get_or_create_browser(self, account: Account, headless: bool = True) -> DeepSeekBrowser:
|
| 76 |
+
try:
|
| 77 |
+
if account.browser is None:
|
| 78 |
+
account.browser = DeepSeekBrowser(
|
| 79 |
+
email=account.email,
|
| 80 |
+
password=account.password,
|
| 81 |
+
profile_dir="./profiles",
|
| 82 |
+
headless=headless,
|
| 83 |
+
humanize=True,
|
| 84 |
+
proxy=account.proxy,
|
| 85 |
+
)
|
| 86 |
+
await account.browser.start()
|
| 87 |
+
account.logged_in = True
|
| 88 |
+
return account.browser
|
| 89 |
+
except Exception as e:
|
| 90 |
+
print(f"Error creating browser: {e}")
|
| 91 |
+
await self.close_browser(account)
|
| 92 |
+
raise
|
| 93 |
+
|
| 94 |
+
async def get_or_create_browser_with_retry(self, account: Account, headless: bool = True) -> DeepSeekBrowser:
|
| 95 |
+
try:
|
| 96 |
+
return await self.get_or_create_browser(account, headless)
|
| 97 |
+
except Exception:
|
| 98 |
+
await self.close_browser(account)
|
| 99 |
+
return await self.get_or_create_browser(account, headless)
|
| 100 |
+
|
| 101 |
+
async def close_browser(self, account: Account):
|
| 102 |
+
if account.browser:
|
| 103 |
+
try:
|
| 104 |
+
await account.browser.close()
|
| 105 |
+
except:
|
| 106 |
+
pass
|
| 107 |
+
account.browser = None
|
| 108 |
+
account.logged_in = False
|
| 109 |
+
|
| 110 |
+
def get_stats(self) -> Dict:
|
| 111 |
+
total = len(self.accounts)
|
| 112 |
+
in_use = sum(1 for a in self.accounts.values() if a.in_use)
|
| 113 |
+
available = sum(1 for a in self.accounts.values() if not a.in_use and a.error_count < 3)
|
| 114 |
+
logged_in = sum(1 for a in self.accounts.values() if a.logged_in)
|
| 115 |
+
return {
|
| 116 |
+
"total": total,
|
| 117 |
+
"in_use": in_use,
|
| 118 |
+
"available": available,
|
| 119 |
+
"logged_in": logged_in,
|
| 120 |
+
"queue_size": len(self.queue),
|
| 121 |
+
}
|
config.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dataclasses import dataclass, field
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@dataclass
|
| 7 |
+
class AccountConfig:
|
| 8 |
+
email: str
|
| 9 |
+
password: str
|
| 10 |
+
name: str = ""
|
| 11 |
+
proxy: Optional[str] = None
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class ServerConfig:
|
| 16 |
+
host: str = "0.0.0.0"
|
| 17 |
+
port: int = 5001
|
| 18 |
+
admin_key: str = "admin"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class BrowserConfig:
|
| 23 |
+
headless: bool = True
|
| 24 |
+
humanize: bool = True
|
| 25 |
+
timeout: int = 60000
|
| 26 |
+
viewport_width: int = 1920
|
| 27 |
+
viewport_height: int = 1080
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class Config:
|
| 32 |
+
server: ServerConfig = field(default_factory=ServerConfig)
|
| 33 |
+
browser: BrowserConfig = field(default_factory=BrowserConfig)
|
| 34 |
+
accounts: List[AccountConfig] = field(default_factory=list)
|
| 35 |
+
api_keys: List[str] = field(default_factory=lambda: ["sk-default"])
|
| 36 |
+
|
| 37 |
+
@classmethod
|
| 38 |
+
def from_env(cls) -> "Config":
|
| 39 |
+
accounts = []
|
| 40 |
+
|
| 41 |
+
account_str = os.getenv("DS2API_ACCOUNTS", "")
|
| 42 |
+
if account_str:
|
| 43 |
+
for acc in account_str.split(";"):
|
| 44 |
+
parts = acc.split(":")
|
| 45 |
+
if len(parts) >= 2:
|
| 46 |
+
accounts.append(AccountConfig(
|
| 47 |
+
email=parts[0],
|
| 48 |
+
password=parts[1],
|
| 49 |
+
name=parts[2] if len(parts) > 2 else "",
|
| 50 |
+
proxy=parts[3] if len(parts) > 3 else None,
|
| 51 |
+
))
|
| 52 |
+
|
| 53 |
+
return cls(
|
| 54 |
+
server=ServerConfig(
|
| 55 |
+
host=os.getenv("DS2API_HOST", "0.0.0.0"),
|
| 56 |
+
port=int(os.getenv("DS2API_PORT", "5001")),
|
| 57 |
+
admin_key=os.getenv("DS2API_ADMIN_KEY", "admin"),
|
| 58 |
+
),
|
| 59 |
+
browser=BrowserConfig(
|
| 60 |
+
headless=os.getenv("DS2API_HEADLESS", "true").lower() == "true",
|
| 61 |
+
humanize=os.getenv("DS2API_HUMANIZE", "true").lower() == "true",
|
| 62 |
+
timeout=int(os.getenv("DS2API_TIMEOUT", "60000")),
|
| 63 |
+
),
|
| 64 |
+
accounts=accounts,
|
| 65 |
+
api_keys=os.getenv("DS2API_KEYS", "sk-default").split(","),
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def load_config() -> Config:
|
| 70 |
+
return Config.from_env()
|
deepseek_browser.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import random
|
| 3 |
+
import time
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import AsyncGenerator, Optional
|
| 6 |
+
|
| 7 |
+
from cloakbrowser import launch_persistent_context_async
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class DeepSeekBrowser:
|
| 11 |
+
DEEPSEEK_URL = "https://chat.deepseek.com"
|
| 12 |
+
|
| 13 |
+
def __init__(
|
| 14 |
+
self,
|
| 15 |
+
email: str,
|
| 16 |
+
password: str,
|
| 17 |
+
profile_dir: str = "./profiles",
|
| 18 |
+
headless: bool = True,
|
| 19 |
+
humanize: bool = True,
|
| 20 |
+
proxy: Optional[str] = None,
|
| 21 |
+
):
|
| 22 |
+
self.email = email
|
| 23 |
+
self.password = password
|
| 24 |
+
self.profile_dir = Path(profile_dir) / email.replace("@", "_at_").replace("+", "_plus_")
|
| 25 |
+
self.headless = headless
|
| 26 |
+
self.humanize = humanize
|
| 27 |
+
self.proxy = proxy
|
| 28 |
+
self.context = None
|
| 29 |
+
self.page = None
|
| 30 |
+
self._logged_in = False
|
| 31 |
+
self._ready = False
|
| 32 |
+
|
| 33 |
+
async def start(self):
|
| 34 |
+
self.profile_dir.mkdir(parents=True, exist_ok=True)
|
| 35 |
+
|
| 36 |
+
self.context = await launch_persistent_context_async(
|
| 37 |
+
user_data_dir=str(self.profile_dir),
|
| 38 |
+
headless=self.headless,
|
| 39 |
+
humanize=self.humanize,
|
| 40 |
+
proxy=self.proxy,
|
| 41 |
+
viewport={"width": 1920, "height": 1080},
|
| 42 |
+
locale="zh-CN",
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
self.page = await self.context.new_page()
|
| 46 |
+
await self.page.goto(self.DEEPSEEK_URL, timeout=60000)
|
| 47 |
+
await asyncio.sleep(5)
|
| 48 |
+
|
| 49 |
+
await self._check_login_state()
|
| 50 |
+
|
| 51 |
+
async def _check_login_state(self):
|
| 52 |
+
current_url = self.page.url
|
| 53 |
+
|
| 54 |
+
if '/sign_in' in current_url:
|
| 55 |
+
await self._auto_login()
|
| 56 |
+
else:
|
| 57 |
+
try:
|
| 58 |
+
await self.page.wait_for_selector('textarea', timeout=10000)
|
| 59 |
+
self._logged_in = True
|
| 60 |
+
self._ready = True
|
| 61 |
+
except Exception:
|
| 62 |
+
await self._auto_login()
|
| 63 |
+
|
| 64 |
+
async def _auto_login(self):
|
| 65 |
+
print(f"Logging in as {self.email}...")
|
| 66 |
+
|
| 67 |
+
try:
|
| 68 |
+
email_input = self.page.locator('input[placeholder*="邮箱"], input[placeholder*="手机"], input[type="text"]').first
|
| 69 |
+
await email_input.wait_for(state="visible", timeout=10000)
|
| 70 |
+
await email_input.fill(self.email)
|
| 71 |
+
await asyncio.sleep(0.5)
|
| 72 |
+
except Exception as e:
|
| 73 |
+
print(f"Email input error: {e}")
|
| 74 |
+
raise
|
| 75 |
+
|
| 76 |
+
try:
|
| 77 |
+
password_input = self.page.locator('input[type="password"]').first
|
| 78 |
+
await password_input.wait_for(state="visible", timeout=5000)
|
| 79 |
+
await password_input.fill(self.password)
|
| 80 |
+
await asyncio.sleep(0.5)
|
| 81 |
+
except Exception as e:
|
| 82 |
+
print(f"Password input error: {e}")
|
| 83 |
+
raise
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
login_button = self.page.locator('button:has-text("登录")').first
|
| 87 |
+
await login_button.click()
|
| 88 |
+
await asyncio.sleep(3)
|
| 89 |
+
except Exception as e:
|
| 90 |
+
print(f"Login button error: {e}")
|
| 91 |
+
raise
|
| 92 |
+
|
| 93 |
+
try:
|
| 94 |
+
await self.page.wait_for_selector('textarea', timeout=30000)
|
| 95 |
+
self._logged_in = True
|
| 96 |
+
self._ready = True
|
| 97 |
+
print("Login successful!")
|
| 98 |
+
except Exception:
|
| 99 |
+
raise Exception("Login failed")
|
| 100 |
+
|
| 101 |
+
async def _human_delay(self, min_ms: int = 300, max_ms: int = 1500):
|
| 102 |
+
delay = random.uniform(min_ms, max_ms) / 1000
|
| 103 |
+
await asyncio.sleep(delay)
|
| 104 |
+
|
| 105 |
+
async def new_chat(self):
|
| 106 |
+
try:
|
| 107 |
+
await self.page.goto(self.DEEPSEEK_URL, timeout=30000)
|
| 108 |
+
await asyncio.sleep(2)
|
| 109 |
+
await self.page.wait_for_selector('textarea', timeout=15000)
|
| 110 |
+
except Exception as e:
|
| 111 |
+
print(f"New chat error: {e}")
|
| 112 |
+
raise
|
| 113 |
+
|
| 114 |
+
async def delete_chat(self):
|
| 115 |
+
try:
|
| 116 |
+
more_btn = self.page.locator('button:has-text("更多"), .ds-icon-button:has-text("...")').first
|
| 117 |
+
if await more_btn.count() > 0:
|
| 118 |
+
await more_btn.click()
|
| 119 |
+
await asyncio.sleep(0.5)
|
| 120 |
+
|
| 121 |
+
delete_btn = self.page.locator('button:has-text("删除"), div:has-text("删除对话")').first
|
| 122 |
+
if await delete_btn.count() > 0:
|
| 123 |
+
await delete_btn.click()
|
| 124 |
+
await asyncio.sleep(0.5)
|
| 125 |
+
|
| 126 |
+
confirm_btn = self.page.locator('button:has-text("确认"), button:has-text("删除")').last
|
| 127 |
+
if await confirm_btn.count() > 0:
|
| 128 |
+
await confirm_btn.click()
|
| 129 |
+
await asyncio.sleep(1)
|
| 130 |
+
except Exception:
|
| 131 |
+
pass
|
| 132 |
+
|
| 133 |
+
async def switch_model(self, model: str):
|
| 134 |
+
try:
|
| 135 |
+
if 'reasoner' in model or 'thinking' in model:
|
| 136 |
+
thinking_btn = self.page.locator('button:has-text("深度思考"), div:has-text("深度思考")').first
|
| 137 |
+
if await thinking_btn.count() > 0:
|
| 138 |
+
await thinking_btn.click()
|
| 139 |
+
await asyncio.sleep(0.5)
|
| 140 |
+
|
| 141 |
+
if 'search' in model:
|
| 142 |
+
search_btn = self.page.locator('button:has-text("智能搜索"), div:has-text("智能搜索")').first
|
| 143 |
+
if await search_btn.count() > 0:
|
| 144 |
+
await search_btn.click()
|
| 145 |
+
await asyncio.sleep(0.5)
|
| 146 |
+
except Exception:
|
| 147 |
+
pass
|
| 148 |
+
|
| 149 |
+
async def send_message(self, prompt: str, timeout: int = 120, model: str = "deepseek-chat") -> str:
|
| 150 |
+
try:
|
| 151 |
+
await self.new_chat()
|
| 152 |
+
await self.switch_model(model)
|
| 153 |
+
|
| 154 |
+
input_field = self.page.locator('textarea').first
|
| 155 |
+
await input_field.wait_for(state="visible", timeout=15000)
|
| 156 |
+
|
| 157 |
+
await self._human_delay(500, 2000)
|
| 158 |
+
|
| 159 |
+
await input_field.clear()
|
| 160 |
+
await input_field.type(prompt, delay=random.randint(30, 80))
|
| 161 |
+
|
| 162 |
+
await self._human_delay(200, 800)
|
| 163 |
+
|
| 164 |
+
await input_field.press('Enter')
|
| 165 |
+
|
| 166 |
+
response = await self._wait_for_response(timeout, prompt)
|
| 167 |
+
|
| 168 |
+
await self.delete_chat()
|
| 169 |
+
|
| 170 |
+
return response
|
| 171 |
+
except Exception as e:
|
| 172 |
+
print(f"Send message error: {e}")
|
| 173 |
+
raise
|
| 174 |
+
|
| 175 |
+
async def _wait_for_response(self, timeout: int, prompt: str = "") -> str:
|
| 176 |
+
deadline = time.time() + timeout
|
| 177 |
+
|
| 178 |
+
await asyncio.sleep(3)
|
| 179 |
+
|
| 180 |
+
last_text = ""
|
| 181 |
+
stable_count = 0
|
| 182 |
+
|
| 183 |
+
skip_phrases = ['深度思考', '智能搜索', '快速模式', '专家模式', '内容由 AI 生成', '开启新对话', '暂无历史对话', '今天', 'huan********dja@gmail.com']
|
| 184 |
+
|
| 185 |
+
while time.time() < deadline:
|
| 186 |
+
try:
|
| 187 |
+
text = await self.page.inner_text('body')
|
| 188 |
+
|
| 189 |
+
lines = text.split('\n')
|
| 190 |
+
response_started = False
|
| 191 |
+
response_text = []
|
| 192 |
+
|
| 193 |
+
for line in lines:
|
| 194 |
+
line = line.strip()
|
| 195 |
+
if not line:
|
| 196 |
+
continue
|
| 197 |
+
|
| 198 |
+
if line == '内容由 AI 生成,请仔细甄别':
|
| 199 |
+
break
|
| 200 |
+
|
| 201 |
+
if any(phrase in line for phrase in skip_phrases):
|
| 202 |
+
continue
|
| 203 |
+
|
| 204 |
+
if response_started:
|
| 205 |
+
response_text.append(line)
|
| 206 |
+
|
| 207 |
+
if prompt and prompt in line:
|
| 208 |
+
response_started = True
|
| 209 |
+
|
| 210 |
+
if response_text:
|
| 211 |
+
current_text = '\n'.join(response_text)
|
| 212 |
+
|
| 213 |
+
if current_text != last_text:
|
| 214 |
+
last_text = current_text
|
| 215 |
+
stable_count = 0
|
| 216 |
+
else:
|
| 217 |
+
stable_count += 1
|
| 218 |
+
|
| 219 |
+
if stable_count >= 3:
|
| 220 |
+
return current_text.strip()
|
| 221 |
+
|
| 222 |
+
except Exception:
|
| 223 |
+
pass
|
| 224 |
+
|
| 225 |
+
await asyncio.sleep(0.5)
|
| 226 |
+
|
| 227 |
+
if last_text:
|
| 228 |
+
return last_text.strip()
|
| 229 |
+
|
| 230 |
+
raise TimeoutError("No response received")
|
| 231 |
+
|
| 232 |
+
async def stream_message(self, prompt: str, timeout: int = 120, model: str = "deepseek-chat") -> AsyncGenerator[str, None]:
|
| 233 |
+
try:
|
| 234 |
+
await self.new_chat()
|
| 235 |
+
await self.switch_model(model)
|
| 236 |
+
|
| 237 |
+
input_field = self.page.locator('textarea').first
|
| 238 |
+
await input_field.wait_for(state="visible", timeout=15000)
|
| 239 |
+
|
| 240 |
+
await self._human_delay(500, 2000)
|
| 241 |
+
|
| 242 |
+
await input_field.clear()
|
| 243 |
+
await input_field.type(prompt, delay=random.randint(30, 80))
|
| 244 |
+
|
| 245 |
+
await self._human_delay(200, 800)
|
| 246 |
+
|
| 247 |
+
await input_field.press('Enter')
|
| 248 |
+
|
| 249 |
+
deadline = time.time() + timeout
|
| 250 |
+
last_text = ""
|
| 251 |
+
stable_count = 0
|
| 252 |
+
|
| 253 |
+
skip_phrases = ['深度思考', '智能搜索', '快速模式', '专家模式', '内容由 AI 生成', '开启新对话', '暂无历史对话', '今天', 'huan********dja@gmail.com']
|
| 254 |
+
|
| 255 |
+
await asyncio.sleep(3)
|
| 256 |
+
|
| 257 |
+
while time.time() < deadline:
|
| 258 |
+
try:
|
| 259 |
+
text = await self.page.inner_text('body')
|
| 260 |
+
|
| 261 |
+
lines = text.split('\n')
|
| 262 |
+
response_started = False
|
| 263 |
+
response_text = []
|
| 264 |
+
|
| 265 |
+
for line in lines:
|
| 266 |
+
line = line.strip()
|
| 267 |
+
if not line:
|
| 268 |
+
continue
|
| 269 |
+
|
| 270 |
+
if line == '内容由 AI 生成,请仔细甄别':
|
| 271 |
+
break
|
| 272 |
+
|
| 273 |
+
if any(phrase in line for phrase in skip_phrases):
|
| 274 |
+
continue
|
| 275 |
+
|
| 276 |
+
if response_started:
|
| 277 |
+
response_text.append(line)
|
| 278 |
+
|
| 279 |
+
if prompt and prompt in line:
|
| 280 |
+
response_started = True
|
| 281 |
+
|
| 282 |
+
if response_text:
|
| 283 |
+
current_text = '\n'.join(response_text)
|
| 284 |
+
|
| 285 |
+
if current_text != last_text:
|
| 286 |
+
new_chunk = current_text[len(last_text):]
|
| 287 |
+
if new_chunk:
|
| 288 |
+
yield new_chunk
|
| 289 |
+
last_text = current_text
|
| 290 |
+
stable_count = 0
|
| 291 |
+
else:
|
| 292 |
+
stable_count += 1
|
| 293 |
+
|
| 294 |
+
if stable_count >= 3:
|
| 295 |
+
return
|
| 296 |
+
|
| 297 |
+
except Exception:
|
| 298 |
+
pass
|
| 299 |
+
|
| 300 |
+
await asyncio.sleep(0.3)
|
| 301 |
+
except Exception as e:
|
| 302 |
+
print(f"Stream message error: {e}")
|
| 303 |
+
raise
|
| 304 |
+
|
| 305 |
+
async def close(self):
|
| 306 |
+
if self.context:
|
| 307 |
+
await self.context.close()
|
main.py
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import json
|
| 3 |
+
import time
|
| 4 |
+
import uuid
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
from fastapi import FastAPI, HTTPException, Header, Request
|
| 8 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
+
from fastapi.responses import StreamingResponse
|
| 10 |
+
from pydantic import BaseModel
|
| 11 |
+
|
| 12 |
+
from account_manager import AccountManager
|
| 13 |
+
from config import Config, load_config
|
| 14 |
+
|
| 15 |
+
app = FastAPI(title="DS2API Browser")
|
| 16 |
+
|
| 17 |
+
app.add_middleware(
|
| 18 |
+
CORSMiddleware,
|
| 19 |
+
allow_origins=["*"],
|
| 20 |
+
allow_credentials=True,
|
| 21 |
+
allow_methods=["*"],
|
| 22 |
+
allow_headers=["*"],
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
config: Config = load_config()
|
| 26 |
+
manager = AccountManager(max_inflight=1)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class Message(BaseModel):
|
| 30 |
+
role: str
|
| 31 |
+
content: str
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class ChatCompletionRequest(BaseModel):
|
| 35 |
+
model: str
|
| 36 |
+
messages: list[Message]
|
| 37 |
+
stream: bool = False
|
| 38 |
+
temperature: Optional[float] = None
|
| 39 |
+
max_tokens: Optional[int] = None
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def verify_api_key(authorization: Optional[str] = Header(None)) -> str:
|
| 43 |
+
if not authorization:
|
| 44 |
+
raise HTTPException(status_code=401, detail="Missing API key")
|
| 45 |
+
|
| 46 |
+
token = authorization.replace("Bearer ", "").strip()
|
| 47 |
+
if token not in config.api_keys:
|
| 48 |
+
raise HTTPException(status_code=401, detail="Invalid API key")
|
| 49 |
+
|
| 50 |
+
return token
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@app.get("/v1/models")
|
| 54 |
+
async def list_models(authorization: str = Header(...)):
|
| 55 |
+
verify_api_key(authorization)
|
| 56 |
+
|
| 57 |
+
return {
|
| 58 |
+
"data": [
|
| 59 |
+
{"id": "deepseek-chat", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
|
| 60 |
+
{"id": "deepseek-reasoner", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
|
| 61 |
+
{"id": "deepseek-v4-flash", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
|
| 62 |
+
{"id": "deepseek-v4-pro", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
|
| 63 |
+
{"id": "deepseek-v4-flash-search", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
|
| 64 |
+
{"id": "deepseek-v4-pro-search", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
|
| 65 |
+
{"id": "deepseek-v4-vision", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
|
| 66 |
+
{"id": "gpt-4o", "object": "model", "created": int(time.time()), "owned_by": "openai"},
|
| 67 |
+
{"id": "gpt-4-turbo", "object": "model", "created": int(time.time()), "owned_by": "openai"},
|
| 68 |
+
{"id": "claude-3-opus", "object": "model", "created": int(time.time()), "owned_by": "anthropic"},
|
| 69 |
+
{"id": "claude-3-sonnet", "object": "model", "created": int(time.time()), "owned_by": "anthropic"},
|
| 70 |
+
{"id": "gemini-pro", "object": "model", "created": int(time.time()), "owned_by": "google"},
|
| 71 |
+
],
|
| 72 |
+
"object": "list",
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@app.get("/v1/models/{model_id}")
|
| 77 |
+
async def get_model(model_id: str, authorization: str = Header(...)):
|
| 78 |
+
verify_api_key(authorization)
|
| 79 |
+
|
| 80 |
+
models = {
|
| 81 |
+
"deepseek-chat": {"id": "deepseek-chat", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
|
| 82 |
+
"deepseek-reasoner": {"id": "deepseek-reasoner", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
|
| 83 |
+
"deepseek-v4-flash": {"id": "deepseek-v4-flash", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
|
| 84 |
+
"deepseek-v4-pro": {"id": "deepseek-v4-pro", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
if model_id in models:
|
| 88 |
+
return models[model_id]
|
| 89 |
+
|
| 90 |
+
raise HTTPException(status_code=404, detail="Model not found")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
@app.post("/v1/chat/completions")
|
| 94 |
+
async def chat_completions(
|
| 95 |
+
request: ChatCompletionRequest,
|
| 96 |
+
authorization: str = Header(...),
|
| 97 |
+
):
|
| 98 |
+
verify_api_key(authorization)
|
| 99 |
+
|
| 100 |
+
if not request.messages:
|
| 101 |
+
raise HTTPException(status_code=400, detail="No messages provided")
|
| 102 |
+
|
| 103 |
+
prompt = request.messages[-1].content
|
| 104 |
+
|
| 105 |
+
account = await manager.acquire()
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
browser = await manager.get_or_create_browser_with_retry(account, headless=config.browser.headless)
|
| 109 |
+
|
| 110 |
+
if request.stream:
|
| 111 |
+
async def stream_with_cleanup():
|
| 112 |
+
chunk_id = f"chatcmpl-{uuid.uuid4().hex[:8]}"
|
| 113 |
+
try:
|
| 114 |
+
async for chunk in browser.stream_message(prompt, timeout=120, model=request.model):
|
| 115 |
+
data = {
|
| 116 |
+
"id": chunk_id,
|
| 117 |
+
"object": "chat.completion.chunk",
|
| 118 |
+
"created": int(time.time()),
|
| 119 |
+
"model": request.model,
|
| 120 |
+
"choices": [
|
| 121 |
+
{
|
| 122 |
+
"index": 0,
|
| 123 |
+
"delta": {"content": chunk},
|
| 124 |
+
"finish_reason": None,
|
| 125 |
+
}
|
| 126 |
+
],
|
| 127 |
+
}
|
| 128 |
+
yield f"data: {json.dumps(data)}\n\n"
|
| 129 |
+
|
| 130 |
+
final_data = {
|
| 131 |
+
"id": chunk_id,
|
| 132 |
+
"object": "chat.completion.chunk",
|
| 133 |
+
"created": int(time.time()),
|
| 134 |
+
"model": request.model,
|
| 135 |
+
"choices": [
|
| 136 |
+
{
|
| 137 |
+
"index": 0,
|
| 138 |
+
"delta": {},
|
| 139 |
+
"finish_reason": "stop",
|
| 140 |
+
}
|
| 141 |
+
],
|
| 142 |
+
}
|
| 143 |
+
yield f"data: {json.dumps(final_data)}\n\n"
|
| 144 |
+
yield "data: [DONE]\n\n"
|
| 145 |
+
except Exception as e:
|
| 146 |
+
yield f"data: {json.dumps({'error': {'message': str(e)}})}\n\n"
|
| 147 |
+
finally:
|
| 148 |
+
await manager.release(account)
|
| 149 |
+
|
| 150 |
+
return StreamingResponse(
|
| 151 |
+
stream_with_cleanup(),
|
| 152 |
+
media_type="text/event-stream",
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
response_text = await browser.send_message(prompt, timeout=120, model=request.model)
|
| 156 |
+
|
| 157 |
+
await manager.release(account)
|
| 158 |
+
|
| 159 |
+
return {
|
| 160 |
+
"id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
|
| 161 |
+
"object": "chat.completion",
|
| 162 |
+
"created": int(time.time()),
|
| 163 |
+
"model": request.model,
|
| 164 |
+
"choices": [
|
| 165 |
+
{
|
| 166 |
+
"index": 0,
|
| 167 |
+
"message": {"role": "assistant", "content": response_text},
|
| 168 |
+
"finish_reason": "stop",
|
| 169 |
+
}
|
| 170 |
+
],
|
| 171 |
+
"usage": {
|
| 172 |
+
"prompt_tokens": len(prompt.split()),
|
| 173 |
+
"completion_tokens": len(response_text.split()),
|
| 174 |
+
"total_tokens": len(prompt.split()) + len(response_text.split()),
|
| 175 |
+
},
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
except Exception as e:
|
| 179 |
+
await manager.mark_error(account)
|
| 180 |
+
raise HTTPException(status_code=503, detail=str(e))
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
@app.get("/anthropic/v1/models")
|
| 184 |
+
async def anthropic_models(authorization: str = Header(...)):
|
| 185 |
+
verify_api_key(authorization)
|
| 186 |
+
|
| 187 |
+
return {
|
| 188 |
+
"data": [
|
| 189 |
+
{"id": "claude-sonnet-4-6", "object": "model", "created": int(time.time()), "owned_by": "anthropic"},
|
| 190 |
+
{"id": "claude-opus-4-6", "object": "model", "created": int(time.time()), "owned_by": "anthropic"},
|
| 191 |
+
{"id": "claude-haiku-4-5", "object": "model", "created": int(time.time()), "owned_by": "anthropic"},
|
| 192 |
+
],
|
| 193 |
+
"object": "list",
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
@app.post("/anthropic/v1/messages")
|
| 198 |
+
async def anthropic_messages(request: Request, authorization: str = Header(...)):
|
| 199 |
+
verify_api_key(authorization)
|
| 200 |
+
|
| 201 |
+
body = await request.json()
|
| 202 |
+
messages = body.get("messages", [])
|
| 203 |
+
model = body.get("model", "claude-sonnet-4-6")
|
| 204 |
+
stream = body.get("stream", False)
|
| 205 |
+
|
| 206 |
+
if not messages:
|
| 207 |
+
raise HTTPException(status_code=400, detail="No messages provided")
|
| 208 |
+
|
| 209 |
+
prompt = messages[-1].get("content", "")
|
| 210 |
+
|
| 211 |
+
account = await manager.acquire()
|
| 212 |
+
|
| 213 |
+
try:
|
| 214 |
+
browser = await manager.get_or_create_browser_with_retry(account, headless=config.browser.headless)
|
| 215 |
+
|
| 216 |
+
if stream:
|
| 217 |
+
async def stream_with_cleanup():
|
| 218 |
+
try:
|
| 219 |
+
async for chunk in browser.stream_message(prompt, timeout=120, model=model):
|
| 220 |
+
data = {
|
| 221 |
+
"type": "content_block_delta",
|
| 222 |
+
"index": 0,
|
| 223 |
+
"delta": {"type": "text_delta", "text": chunk},
|
| 224 |
+
}
|
| 225 |
+
yield f"event: content_block_delta\ndata: {json.dumps(data)}\n\n"
|
| 226 |
+
|
| 227 |
+
yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n"
|
| 228 |
+
except Exception as e:
|
| 229 |
+
yield f"event: error\ndata: {json.dumps({'type': 'error', 'error': {'type': 'server_error', 'message': str(e)}})}\n\n"
|
| 230 |
+
finally:
|
| 231 |
+
await manager.release(account)
|
| 232 |
+
|
| 233 |
+
return StreamingResponse(
|
| 234 |
+
stream_with_cleanup(),
|
| 235 |
+
media_type="text/event-stream",
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
response_text = await browser.send_message(prompt, timeout=120, model=model)
|
| 239 |
+
|
| 240 |
+
await manager.release(account)
|
| 241 |
+
|
| 242 |
+
return {
|
| 243 |
+
"id": f"msg_{uuid.uuid4().hex[:8]}",
|
| 244 |
+
"type": "message",
|
| 245 |
+
"role": "assistant",
|
| 246 |
+
"model": model,
|
| 247 |
+
"content": [{"type": "text", "text": response_text}],
|
| 248 |
+
"stop_reason": "end_turn",
|
| 249 |
+
"usage": {
|
| 250 |
+
"input_tokens": len(prompt.split()),
|
| 251 |
+
"output_tokens": len(response_text.split()),
|
| 252 |
+
},
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
except Exception as e:
|
| 256 |
+
await manager.mark_error(account)
|
| 257 |
+
raise HTTPException(status_code=503, detail=str(e))
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
@app.post("/v1beta/models/{model}:generateContent")
|
| 261 |
+
async def gemini_generate(model: str, request: Request, authorization: str = Header(...)):
|
| 262 |
+
verify_api_key(authorization)
|
| 263 |
+
|
| 264 |
+
body = await request.json()
|
| 265 |
+
contents = body.get("contents", [])
|
| 266 |
+
|
| 267 |
+
if not contents:
|
| 268 |
+
raise HTTPException(status_code=400, detail="No contents provided")
|
| 269 |
+
|
| 270 |
+
prompt = contents[-1].get("parts", [{}])[0].get("text", "")
|
| 271 |
+
|
| 272 |
+
account = await manager.acquire()
|
| 273 |
+
|
| 274 |
+
try:
|
| 275 |
+
browser = await manager.get_or_create_browser_with_retry(account, headless=config.browser.headless)
|
| 276 |
+
|
| 277 |
+
response_text = await browser.send_message(prompt, timeout=120, model=model)
|
| 278 |
+
|
| 279 |
+
await manager.release(account)
|
| 280 |
+
|
| 281 |
+
return {
|
| 282 |
+
"candidates": [
|
| 283 |
+
{
|
| 284 |
+
"content": {
|
| 285 |
+
"parts": [{"text": response_text}],
|
| 286 |
+
"role": "model",
|
| 287 |
+
},
|
| 288 |
+
"finishReason": "STOP",
|
| 289 |
+
}
|
| 290 |
+
],
|
| 291 |
+
"usageMetadata": {
|
| 292 |
+
"promptTokenCount": len(prompt.split()),
|
| 293 |
+
"candidatesTokenCount": len(response_text.split()),
|
| 294 |
+
"totalTokenCount": len(prompt.split()) + len(response_text.split()),
|
| 295 |
+
},
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
except Exception as e:
|
| 299 |
+
await manager.mark_error(account)
|
| 300 |
+
raise HTTPException(status_code=503, detail=str(e))
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
@app.post("/v1beta/models/{model}:streamGenerateContent")
|
| 304 |
+
async def gemini_stream_generate(model: str, request: Request, authorization: str = Header(...)):
|
| 305 |
+
verify_api_key(authorization)
|
| 306 |
+
|
| 307 |
+
body = await request.json()
|
| 308 |
+
contents = body.get("contents", [])
|
| 309 |
+
|
| 310 |
+
if not contents:
|
| 311 |
+
raise HTTPException(status_code=400, detail="No contents provided")
|
| 312 |
+
|
| 313 |
+
prompt = contents[-1].get("parts", [{}])[0].get("text", "")
|
| 314 |
+
|
| 315 |
+
account = await manager.acquire()
|
| 316 |
+
|
| 317 |
+
try:
|
| 318 |
+
browser = await manager.get_or_create_browser_with_retry(account, headless=config.browser.headless)
|
| 319 |
+
|
| 320 |
+
async def stream_with_cleanup():
|
| 321 |
+
try:
|
| 322 |
+
async for chunk in browser.stream_message(prompt, timeout=120, model=model):
|
| 323 |
+
data = {
|
| 324 |
+
"candidates": [
|
| 325 |
+
{
|
| 326 |
+
"content": {
|
| 327 |
+
"parts": [{"text": chunk}],
|
| 328 |
+
"role": "model",
|
| 329 |
+
},
|
| 330 |
+
}
|
| 331 |
+
],
|
| 332 |
+
}
|
| 333 |
+
yield f"data: {json.dumps(data)}\n\n"
|
| 334 |
+
|
| 335 |
+
final_data = {
|
| 336 |
+
"candidates": [
|
| 337 |
+
{
|
| 338 |
+
"content": {"parts": [], "role": "model"},
|
| 339 |
+
"finishReason": "STOP",
|
| 340 |
+
}
|
| 341 |
+
],
|
| 342 |
+
"usageMetadata": {
|
| 343 |
+
"promptTokenCount": 0,
|
| 344 |
+
"candidatesTokenCount": 0,
|
| 345 |
+
"totalTokenCount": 0,
|
| 346 |
+
},
|
| 347 |
+
}
|
| 348 |
+
yield f"data: {json.dumps(final_data)}\n\n"
|
| 349 |
+
except Exception as e:
|
| 350 |
+
yield f"data: {json.dumps({'error': {'message': str(e)}})}\n\n"
|
| 351 |
+
finally:
|
| 352 |
+
await manager.release(account)
|
| 353 |
+
|
| 354 |
+
return StreamingResponse(
|
| 355 |
+
stream_with_cleanup(),
|
| 356 |
+
media_type="text/event-stream",
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
except Exception as e:
|
| 360 |
+
await manager.mark_error(account)
|
| 361 |
+
raise HTTPException(status_code=503, detail=str(e))
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
@app.get("/api/version")
|
| 365 |
+
async def ollama_version():
|
| 366 |
+
return {"version": "0.1.0"}
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
@app.get("/api/tags")
|
| 370 |
+
async def ollama_tags():
|
| 371 |
+
return {
|
| 372 |
+
"models": [
|
| 373 |
+
{"name": "deepseek-chat", "model": "deepseek-chat"},
|
| 374 |
+
{"name": "deepseek-reasoner", "model": "deepseek-reasoner"},
|
| 375 |
+
]
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
@app.post("/api/show")
|
| 380 |
+
async def ollama_show(request: Request):
|
| 381 |
+
body = await request.json()
|
| 382 |
+
model = body.get("model", "deepseek-chat")
|
| 383 |
+
|
| 384 |
+
return {
|
| 385 |
+
"id": model,
|
| 386 |
+
"capabilities": ["tools", "thinking"],
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
@app.get("/healthz")
|
| 391 |
+
async def healthz():
|
| 392 |
+
return {"status": "ok"}
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
@app.get("/readyz")
|
| 396 |
+
async def readyz():
|
| 397 |
+
stats = manager.get_stats()
|
| 398 |
+
return {"status": "ok", "accounts": stats}
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
@app.get("/admin/stats")
|
| 402 |
+
async def admin_stats(admin_key: str = Header(...)):
|
| 403 |
+
if admin_key != config.server.admin_key:
|
| 404 |
+
raise HTTPException(status_code=401, detail="Invalid admin key")
|
| 405 |
+
|
| 406 |
+
return manager.get_stats()
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
@app.post("/admin/accounts/import")
|
| 410 |
+
async def import_accounts(request: Request, admin_key: str = Header(...)):
|
| 411 |
+
if admin_key != config.server.admin_key:
|
| 412 |
+
raise HTTPException(status_code=401, detail="Invalid admin key")
|
| 413 |
+
|
| 414 |
+
body = await request.json()
|
| 415 |
+
accounts = body.get("accounts", [])
|
| 416 |
+
|
| 417 |
+
if not accounts:
|
| 418 |
+
raise HTTPException(status_code=400, detail="No accounts provided")
|
| 419 |
+
|
| 420 |
+
imported = 0
|
| 421 |
+
for acc in accounts:
|
| 422 |
+
email = acc.get("email")
|
| 423 |
+
password = acc.get("password")
|
| 424 |
+
name = acc.get("name", "")
|
| 425 |
+
proxy = acc.get("proxy")
|
| 426 |
+
|
| 427 |
+
if email and password:
|
| 428 |
+
manager.add_account(email, password, name, proxy)
|
| 429 |
+
imported += 1
|
| 430 |
+
|
| 431 |
+
return {"success": True, "imported": imported, "total": len(manager.accounts)}
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
@app.get("/admin/accounts")
|
| 435 |
+
async def list_accounts(admin_key: str = Header(...)):
|
| 436 |
+
if admin_key != config.server.admin_key:
|
| 437 |
+
raise HTTPException(status_code=401, detail="Invalid admin key")
|
| 438 |
+
|
| 439 |
+
accounts = []
|
| 440 |
+
for email, acc in manager.accounts.items():
|
| 441 |
+
accounts.append({
|
| 442 |
+
"email": email,
|
| 443 |
+
"name": acc.name,
|
| 444 |
+
"in_use": acc.in_use,
|
| 445 |
+
"logged_in": acc.logged_in,
|
| 446 |
+
"error_count": acc.error_count,
|
| 447 |
+
})
|
| 448 |
+
|
| 449 |
+
return {"accounts": accounts, "total": len(accounts)}
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
@app.on_event("startup")
|
| 453 |
+
async def startup():
|
| 454 |
+
for acc in config.accounts:
|
| 455 |
+
manager.add_account(
|
| 456 |
+
email=acc.email,
|
| 457 |
+
password=acc.password,
|
| 458 |
+
name=acc.name,
|
| 459 |
+
proxy=acc.proxy,
|
| 460 |
+
)
|
| 461 |
+
|
| 462 |
+
print(f"Loaded {len(config.accounts)} accounts")
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
def main():
|
| 466 |
+
import uvicorn
|
| 467 |
+
|
| 468 |
+
uvicorn.run(
|
| 469 |
+
app,
|
| 470 |
+
host=config.server.host,
|
| 471 |
+
port=config.server.port,
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
if __name__ == "__main__":
|
| 476 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cloakbrowser>=0.3.0
|
| 2 |
+
fastapi>=0.100.0
|
| 3 |
+
uvicorn>=0.23.0
|
| 4 |
+
pydantic>=2.0.0
|
| 5 |
+
python-dotenv>=1.0.0
|
run.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 6 |
+
|
| 7 |
+
from main import main
|
| 8 |
+
|
| 9 |
+
if __name__ == "__main__":
|
| 10 |
+
main()
|
start.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Quick test script for DS2API Browser with multiple accounts."""
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 7 |
+
|
| 8 |
+
# 多账号配置,用分号分隔
|
| 9 |
+
# 格式: email:password:name:proxy
|
| 10 |
+
os.environ["DS2API_ACCOUNTS"] = "huanxiangnb+dja@gmail.com:m1234567:账号1;huanxiangnb+321fffww@gmail.com:m1234567:账号2"
|
| 11 |
+
os.environ["DS2API_KEYS"] = "sk-test123456"
|
| 12 |
+
os.environ["DS2API_ADMIN_KEY"] = "admin"
|
| 13 |
+
os.environ["DS2API_PORT"] = "5002"
|
| 14 |
+
os.environ["DS2API_HEADLESS"] = "true"
|
| 15 |
+
|
| 16 |
+
from main import main
|
| 17 |
+
|
| 18 |
+
if __name__ == "__main__":
|
| 19 |
+
main()
|
test.html
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>DS2API Browser 测试</title>
|
| 7 |
+
<style>
|
| 8 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 9 |
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; }
|
| 10 |
+
.container { max-width: 800px; margin: 0 auto; }
|
| 11 |
+
h1 { text-align: center; margin-bottom: 20px; color: #333; }
|
| 12 |
+
.card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
| 13 |
+
.form-group { margin-bottom: 15px; }
|
| 14 |
+
label { display: block; margin-bottom: 5px; font-weight: 500; color: #555; }
|
| 15 |
+
input, textarea, select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
|
| 16 |
+
textarea { min-height: 100px; resize: vertical; }
|
| 17 |
+
button { background: #007bff; color: white; border: none; padding: 12px 24px; border-radius: 4px; cursor: pointer; font-size: 16px; width: 100%; }
|
| 18 |
+
button:hover { background: #0056b3; }
|
| 19 |
+
button:disabled { background: #ccc; cursor: not-allowed; }
|
| 20 |
+
.response { background: #f8f9fa; border-radius: 4px; padding: 15px; margin-top: 15px; white-space: pre-wrap; word-wrap: break-word; min-height: 50px; }
|
| 21 |
+
.loading { text-align: center; color: #666; }
|
| 22 |
+
.error { color: #dc3545; }
|
| 23 |
+
.success { color: #28a745; }
|
| 24 |
+
.info { display: flex; justify-content: space-between; margin-top: 10px; font-size: 12px; color: #888; }
|
| 25 |
+
.checkbox-group { display: flex; align-items: center; gap: 10px; }
|
| 26 |
+
.checkbox-group input { width: auto; }
|
| 27 |
+
</style>
|
| 28 |
+
</head>
|
| 29 |
+
<body>
|
| 30 |
+
<div class="container">
|
| 31 |
+
<h1>🤖 DS2API Browser 测试</h1>
|
| 32 |
+
|
| 33 |
+
<div class="card">
|
| 34 |
+
<div class="form-group">
|
| 35 |
+
<label>API 地址</label>
|
| 36 |
+
<input type="text" id="apiUrl" value="http://localhost:5002">
|
| 37 |
+
</div>
|
| 38 |
+
<div class="form-group">
|
| 39 |
+
<label>API Key</label>
|
| 40 |
+
<input type="text" id="apiKey" value="sk-test123456">
|
| 41 |
+
</div>
|
| 42 |
+
<div class="form-group">
|
| 43 |
+
<label>模型</label>
|
| 44 |
+
<select id="model">
|
| 45 |
+
<option value="deepseek-v4-flash">deepseek-v4-flash (默认)</option>
|
| 46 |
+
<option value="deepseek-v4-pro">deepseek-v4-pro (专家)</option>
|
| 47 |
+
<option value="deepseek-v4-flash-search">deepseek-v4-flash-search</option>
|
| 48 |
+
<option value="deepseek-v4-pro-search">deepseek-v4-pro-search</option>
|
| 49 |
+
</select>
|
| 50 |
+
</div>
|
| 51 |
+
<div class="form-group">
|
| 52 |
+
<div class="checkbox-group">
|
| 53 |
+
<input type="checkbox" id="stream" checked>
|
| 54 |
+
<label for="stream">流式响应</label>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
<div class="form-group">
|
| 58 |
+
<label>消息</label>
|
| 59 |
+
<textarea id="message" placeholder="输入你的消息...">你好,请介绍一下你自己</textarea>
|
| 60 |
+
</div>
|
| 61 |
+
<button id="sendBtn" onclick="sendMessage()">发送消息</button>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<div class="card">
|
| 65 |
+
<label>响应</label>
|
| 66 |
+
<div id="response" class="response">等待发送...</div>
|
| 67 |
+
<div class="info">
|
| 68 |
+
<span id="status"></span>
|
| 69 |
+
<span id="time"></span>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<div class="card">
|
| 74 |
+
<label>账号管理</label>
|
| 75 |
+
<div class="form-group" style="margin-top: 10px;">
|
| 76 |
+
<label>导入账号 (格式: email:password,每行一个)</label>
|
| 77 |
+
<textarea id="accountsInput" rows="4" placeholder="user1@gmail.com:password1 user2@gmail.com:password2"></textarea>
|
| 78 |
+
</div>
|
| 79 |
+
<div style="display: flex; gap: 10px;">
|
| 80 |
+
<button onclick="importAccounts()" style="flex: 1;">导入账号</button>
|
| 81 |
+
<button onclick="loadAccounts()" style="flex: 1; background: #6c757d;">刷新账号列表</button>
|
| 82 |
+
</div>
|
| 83 |
+
<div id="accountsList" class="response" style="margin-top: 10px; max-height: 200px; overflow-y: auto;">点击"刷新账号列表"查看...</div>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<script>
|
| 88 |
+
async function sendMessage() {
|
| 89 |
+
const apiUrl = document.getElementById('apiUrl').value;
|
| 90 |
+
const apiKey = document.getElementById('apiKey').value;
|
| 91 |
+
const model = document.getElementById('model').value;
|
| 92 |
+
const message = document.getElementById('message').value;
|
| 93 |
+
const isStream = document.getElementById('stream').checked;
|
| 94 |
+
const responseDiv = document.getElementById('response');
|
| 95 |
+
const statusSpan = document.getElementById('status');
|
| 96 |
+
const timeSpan = document.getElementById('time');
|
| 97 |
+
const sendBtn = document.getElementById('sendBtn');
|
| 98 |
+
|
| 99 |
+
if (!message.trim()) {
|
| 100 |
+
responseDiv.textContent = '请输入消息';
|
| 101 |
+
responseDiv.className = 'response error';
|
| 102 |
+
return;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
sendBtn.disabled = true;
|
| 106 |
+
sendBtn.textContent = '发送中...';
|
| 107 |
+
responseDiv.textContent = '正在等待响应...';
|
| 108 |
+
responseDiv.className = 'response loading';
|
| 109 |
+
statusSpan.textContent = '';
|
| 110 |
+
timeSpan.textContent = '';
|
| 111 |
+
|
| 112 |
+
const startTime = Date.now();
|
| 113 |
+
|
| 114 |
+
try {
|
| 115 |
+
const response = await fetch(`${apiUrl}/v1/chat/completions`, {
|
| 116 |
+
method: 'POST',
|
| 117 |
+
headers: {
|
| 118 |
+
'Content-Type': 'application/json',
|
| 119 |
+
'Authorization': `Bearer ${apiKey}`
|
| 120 |
+
},
|
| 121 |
+
body: JSON.stringify({
|
| 122 |
+
model: model,
|
| 123 |
+
messages: [{ role: 'user', content: message }],
|
| 124 |
+
stream: isStream
|
| 125 |
+
})
|
| 126 |
+
});
|
| 127 |
+
|
| 128 |
+
if (isStream) {
|
| 129 |
+
const reader = response.body.getReader();
|
| 130 |
+
const decoder = new TextDecoder();
|
| 131 |
+
let fullContent = '';
|
| 132 |
+
|
| 133 |
+
while (true) {
|
| 134 |
+
const { done, value } = await reader.read();
|
| 135 |
+
if (done) break;
|
| 136 |
+
|
| 137 |
+
const chunk = decoder.decode(value, { stream: true });
|
| 138 |
+
const lines = chunk.split('\n');
|
| 139 |
+
|
| 140 |
+
for (const line of lines) {
|
| 141 |
+
if (line.startsWith('data: ')) {
|
| 142 |
+
const data = line.slice(6).trim();
|
| 143 |
+
if (data === '[DONE]') continue;
|
| 144 |
+
|
| 145 |
+
try {
|
| 146 |
+
const json = JSON.parse(data);
|
| 147 |
+
const content = json.choices?.[0]?.delta?.content;
|
| 148 |
+
if (content) {
|
| 149 |
+
fullContent += content;
|
| 150 |
+
responseDiv.textContent = fullContent;
|
| 151 |
+
responseDiv.className = 'response success';
|
| 152 |
+
}
|
| 153 |
+
} catch (e) {
|
| 154 |
+
// Skip invalid JSON
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
| 161 |
+
statusSpan.textContent = `状态: 流式完成`;
|
| 162 |
+
timeSpan.textContent = `耗时: ${elapsed}s`;
|
| 163 |
+
} else {
|
| 164 |
+
const data = await response.json();
|
| 165 |
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
| 166 |
+
|
| 167 |
+
if (response.ok) {
|
| 168 |
+
const content = data.choices?.[0]?.message?.content || '无响应内容';
|
| 169 |
+
responseDiv.textContent = content;
|
| 170 |
+
responseDiv.className = 'response success';
|
| 171 |
+
statusSpan.textContent = `状态: ${response.status || 200} OK`;
|
| 172 |
+
} else {
|
| 173 |
+
responseDiv.textContent = `错误: ${data.detail || JSON.stringify(data)}`;
|
| 174 |
+
responseDiv.className = 'response error';
|
| 175 |
+
statusSpan.textContent = `状态: ${response.status || '错误'}`;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
timeSpan.textContent = `耗时: ${elapsed}s`;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
} catch (error) {
|
| 182 |
+
responseDiv.textContent = `请求失败: ${error.message}`;
|
| 183 |
+
responseDiv.className = 'response error';
|
| 184 |
+
statusSpan.textContent = '错误';
|
| 185 |
+
} finally {
|
| 186 |
+
sendBtn.disabled = false;
|
| 187 |
+
sendBtn.textContent = '发送消息';
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
document.getElementById('message').addEventListener('keydown', function(e) {
|
| 192 |
+
if (e.ctrlKey && e.key === 'Enter') {
|
| 193 |
+
sendMessage();
|
| 194 |
+
}
|
| 195 |
+
});
|
| 196 |
+
|
| 197 |
+
async function importAccounts() {
|
| 198 |
+
const apiUrl = document.getElementById('apiUrl').value;
|
| 199 |
+
const apiKey = document.getElementById('apiKey').value;
|
| 200 |
+
const accountsText = document.getElementById('accountsInput').value.trim();
|
| 201 |
+
const accountsList = document.getElementById('accountsList');
|
| 202 |
+
|
| 203 |
+
if (!accountsText) {
|
| 204 |
+
accountsList.textContent = '请输入账号信息';
|
| 205 |
+
accountsList.className = 'response error';
|
| 206 |
+
return;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
const lines = accountsText.split('\n').filter(line => line.trim());
|
| 210 |
+
const accounts = [];
|
| 211 |
+
|
| 212 |
+
for (const line of lines) {
|
| 213 |
+
const [email, password] = line.split(':');
|
| 214 |
+
if (email && password) {
|
| 215 |
+
accounts.push({ email: email.trim(), password: password.trim() });
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
if (accounts.length === 0) {
|
| 220 |
+
accountsList.textContent = '格式错误,请使用 email:password 格式';
|
| 221 |
+
accountsList.className = 'response error';
|
| 222 |
+
return;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
accountsList.textContent = '导入中...';
|
| 226 |
+
accountsList.className = 'response loading';
|
| 227 |
+
|
| 228 |
+
try {
|
| 229 |
+
const response = await fetch(`${apiUrl}/admin/accounts/import`, {
|
| 230 |
+
method: 'POST',
|
| 231 |
+
headers: {
|
| 232 |
+
'Content-Type': 'application/json',
|
| 233 |
+
'Authorization': `Bearer ${apiKey}`,
|
| 234 |
+
'admin-key': 'admin'
|
| 235 |
+
},
|
| 236 |
+
body: JSON.stringify({ accounts })
|
| 237 |
+
});
|
| 238 |
+
|
| 239 |
+
const data = await response.json();
|
| 240 |
+
|
| 241 |
+
if (response.ok) {
|
| 242 |
+
accountsList.textContent = JSON.stringify(data, null, 2);
|
| 243 |
+
accountsList.className = 'response success';
|
| 244 |
+
document.getElementById('accountsInput').value = '';
|
| 245 |
+
} else {
|
| 246 |
+
accountsList.textContent = `错误: ${data.detail || JSON.stringify(data)}`;
|
| 247 |
+
accountsList.className = 'response error';
|
| 248 |
+
}
|
| 249 |
+
} catch (error) {
|
| 250 |
+
accountsList.textContent = `请求失败: ${error.message}`;
|
| 251 |
+
accountsList.className = 'response error';
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
async function loadAccounts() {
|
| 256 |
+
const apiUrl = document.getElementById('apiUrl').value;
|
| 257 |
+
const apiKey = document.getElementById('apiKey').value;
|
| 258 |
+
const accountsList = document.getElementById('accountsList');
|
| 259 |
+
|
| 260 |
+
accountsList.textContent = '加载中...';
|
| 261 |
+
accountsList.className = 'response loading';
|
| 262 |
+
|
| 263 |
+
try {
|
| 264 |
+
const response = await fetch(`${apiUrl}/admin/accounts`, {
|
| 265 |
+
method: 'GET',
|
| 266 |
+
headers: {
|
| 267 |
+
'Authorization': `Bearer ${apiKey}`,
|
| 268 |
+
'admin-key': 'admin'
|
| 269 |
+
}
|
| 270 |
+
});
|
| 271 |
+
|
| 272 |
+
const data = await response.json();
|
| 273 |
+
|
| 274 |
+
if (response.ok) {
|
| 275 |
+
let html = `账号数量: ${data.total}\n\n`;
|
| 276 |
+
for (const acc of data.accounts) {
|
| 277 |
+
html += `邮箱: ${acc.email}\n状态: ${acc.status}\n使用次数: ${acc.usage_count}\n\n`;
|
| 278 |
+
}
|
| 279 |
+
accountsList.textContent = html;
|
| 280 |
+
accountsList.className = 'response success';
|
| 281 |
+
} else {
|
| 282 |
+
accountsList.textContent = `错误: ${data.detail || JSON.stringify(data)}`;
|
| 283 |
+
accountsList.className = 'response error';
|
| 284 |
+
}
|
| 285 |
+
} catch (error) {
|
| 286 |
+
accountsList.textContent = `请求失败: ${error.message}`;
|
| 287 |
+
accountsList.className = 'response error';
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
</script>
|
| 291 |
+
</body>
|
| 292 |
+
</html>
|