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

Files changed (10) hide show
  1. .gitignore +17 -0
  2. README.md +138 -0
  3. account_manager.py +121 -0
  4. config.py +70 -0
  5. deepseek_browser.py +307 -0
  6. main.py +476 -0
  7. requirements.txt +5 -0
  8. run.py +10 -0
  9. start.py +19 -0
  10. 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&#10;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>