nacho commited on
Commit
5cba267
·
1 Parent(s): 078a298

refactor: use DS2API proxy for reliable backend

Browse files

- Add proxy.py to forward requests to original DS2API
- Keep all API endpoints (OpenAI, Claude, Gemini, Ollama)
- Add account mute detection
- Fix streaming by creating client inside generator

Files changed (5) hide show
  1. account_manager.py +19 -25
  2. deepseek_api.py +261 -0
  3. deepseek_browser.py +106 -85
  4. main.py +39 -12
  5. proxy.py +339 -0
account_manager.py CHANGED
@@ -1,10 +1,9 @@
1
  import asyncio
2
- import random
3
  from collections import deque
4
  from dataclasses import dataclass, field
5
  from typing import Dict, Optional
6
 
7
- from deepseek_browser import DeepSeekBrowser
8
 
9
 
10
  @dataclass
@@ -13,9 +12,9 @@ class Account:
13
  password: str
14
  name: str = ""
15
  proxy: Optional[str] = None
16
- browser: Optional[DeepSeekBrowser] = field(default=None, repr=False)
17
- in_use_count: int = 0 # 当前并发数
18
- max_concurrent: int = 3 # 最大并发数
19
  error_count: int = 0
20
  logged_in: bool = False
21
 
@@ -38,13 +37,11 @@ class AccountManager:
38
 
39
  async def acquire(self) -> Account:
40
  async with self._lock:
41
- # 找一个可用的账号(并发数未满)
42
  for account in self.accounts.values():
43
  if account.in_use_count < account.max_concurrent and account.error_count < 3:
44
  account.in_use_count += 1
45
  return account
46
 
47
- # 没有可用账号,等待
48
  return await self._wait_for_account()
49
 
50
  async def _wait_for_account(self) -> Account:
@@ -77,39 +74,36 @@ class AccountManager:
77
  event = self.queue.popleft()
78
  event.set()
79
 
80
- async def get_or_create_browser(self, account: Account, headless: bool = True) -> DeepSeekBrowser:
81
  try:
82
- if account.browser is None:
83
- account.browser = DeepSeekBrowser(
84
  email=account.email,
85
  password=account.password,
86
- profile_dir="./profiles",
87
- headless=headless,
88
- humanize=True,
89
  proxy=account.proxy,
90
  )
91
- await account.browser.start()
92
  account.logged_in = True
93
- return account.browser
94
  except Exception as e:
95
- print(f"Error creating browser: {e}")
96
- await self.close_browser(account)
97
  raise
98
 
99
- async def get_or_create_browser_with_retry(self, account: Account, headless: bool = True) -> DeepSeekBrowser:
100
  try:
101
- return await self.get_or_create_browser(account, headless)
102
  except Exception:
103
- await self.close_browser(account)
104
- return await self.get_or_create_browser(account, headless)
105
 
106
- async def close_browser(self, account: Account):
107
- if account.browser:
108
  try:
109
- await account.browser.close()
110
  except:
111
  pass
112
- account.browser = None
113
  account.logged_in = False
114
 
115
  def get_stats(self) -> Dict:
 
1
  import asyncio
 
2
  from collections import deque
3
  from dataclasses import dataclass, field
4
  from typing import Dict, Optional
5
 
6
+ from deepseek_api import DeepSeekAPI
7
 
8
 
9
  @dataclass
 
12
  password: str
13
  name: str = ""
14
  proxy: Optional[str] = None
15
+ api: Optional[DeepSeekAPI] = field(default=None, repr=False)
16
+ in_use_count: int = 0
17
+ max_concurrent: int = 3
18
  error_count: int = 0
19
  logged_in: bool = False
20
 
 
37
 
38
  async def acquire(self) -> Account:
39
  async with self._lock:
 
40
  for account in self.accounts.values():
41
  if account.in_use_count < account.max_concurrent and account.error_count < 3:
42
  account.in_use_count += 1
43
  return account
44
 
 
45
  return await self._wait_for_account()
46
 
47
  async def _wait_for_account(self) -> Account:
 
74
  event = self.queue.popleft()
75
  event.set()
76
 
77
+ async def get_api(self, account: Account) -> DeepSeekAPI:
78
  try:
79
+ if account.api is None:
80
+ account.api = DeepSeekAPI(
81
  email=account.email,
82
  password=account.password,
 
 
 
83
  proxy=account.proxy,
84
  )
85
+ await account.api.login()
86
  account.logged_in = True
87
+ return account.api
88
  except Exception as e:
89
+ print(f"Error creating API: {e}")
90
+ await self.close_api(account)
91
  raise
92
 
93
+ async def get_api_with_retry(self, account: Account) -> DeepSeekAPI:
94
  try:
95
+ return await self.get_api(account)
96
  except Exception:
97
+ await self.close_api(account)
98
+ return await self.get_api(account)
99
 
100
+ async def close_api(self, account: Account):
101
+ if account.api:
102
  try:
103
+ await account.api.close()
104
  except:
105
  pass
106
+ account.api = None
107
  account.logged_in = False
108
 
109
  def get_stats(self) -> Dict:
deepseek_api.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import hashlib
3
+ import json
4
+ import random
5
+ import time
6
+ import uuid
7
+ from typing import AsyncGenerator, Optional
8
+
9
+ import httpx
10
+
11
+
12
+ class DeepSeekAPI:
13
+ LOGIN_URL = "https://chat.deepseek.com/api/v0/users/login"
14
+ CREATE_SESSION_URL = "https://chat.deepseek.com/api/v0/chat_session/create"
15
+ CREATE_POW_URL = "https://chat.deepseek.com/api/v0/chat/create_pow_challenge"
16
+ COMPLETION_URL = "https://chat.deepseek.com/api/v0/chat/completion"
17
+ DELETE_SESSION_URL = "https://chat.deepseek.com/api/v0/chat_session/delete"
18
+
19
+ def __init__(self, email: str, password: str, proxy: Optional[str] = None):
20
+ self.email = email
21
+ self.password = password
22
+ self.proxy = proxy
23
+ self._token: Optional[str] = None
24
+ self._device_id = str(uuid.uuid4())
25
+ self._client: Optional[httpx.AsyncClient] = None
26
+
27
+ async def _get_client(self) -> httpx.AsyncClient:
28
+ if self._client is None or self._client.is_closed:
29
+ self._client = httpx.AsyncClient(
30
+ proxy=self.proxy,
31
+ timeout=60.0,
32
+ follow_redirects=True,
33
+ )
34
+ return self._client
35
+
36
+ async def login(self) -> str:
37
+ client = await self._get_client()
38
+
39
+ payload = {
40
+ "email": self.email,
41
+ "password": self.password,
42
+ "device_id": self._device_id,
43
+ "os": "android",
44
+ }
45
+
46
+ headers = self._base_headers()
47
+
48
+ resp = await client.post(self.LOGIN_URL, json=payload, headers=headers, timeout=30)
49
+ data = resp.json()
50
+
51
+ code = data.get("code", -1)
52
+ if code != 0:
53
+ raise Exception(f"Login failed: {data.get('msg', 'Unknown')}")
54
+
55
+ biz_data = data.get("data", {})
56
+ biz_code = biz_data.get("biz_code", -1)
57
+ if biz_code != 0:
58
+ raise Exception(f"Login failed: {biz_data.get('biz_msg', 'Unknown')}")
59
+
60
+ user = biz_data.get("biz_data", {}).get("user", {})
61
+ self._token = user.get("token", "")
62
+
63
+ if not self._token:
64
+ raise Exception("No token received")
65
+
66
+ return self._token
67
+
68
+ async def create_session(self) -> str:
69
+ if not self._token:
70
+ await self.login()
71
+
72
+ client = await self._get_client()
73
+ headers = self._auth_headers()
74
+
75
+ resp = await client.post(
76
+ self.CREATE_SESSION_URL,
77
+ json={"agent": "chat"},
78
+ headers=headers,
79
+ timeout=30,
80
+ )
81
+ data = resp.json()
82
+
83
+ if data.get("code") != 0:
84
+ raise Exception(f"Create session failed: {data.get('msg')}")
85
+
86
+ biz_data = data.get("data", {}).get("biz_data", {})
87
+
88
+ if "chat_session" in biz_data:
89
+ session_id = biz_data["chat_session"].get("id", "")
90
+ else:
91
+ session_id = biz_data.get("id", "")
92
+
93
+ if not session_id:
94
+ raise Exception("No session ID received")
95
+
96
+ return session_id
97
+
98
+ async def get_pow(self, target_path: str = "/api/v0/chat/completion") -> dict:
99
+ client = await self._get_client()
100
+ headers = self._auth_headers()
101
+
102
+ resp = await client.post(
103
+ self.CREATE_POW_URL,
104
+ json={"target_path": target_path},
105
+ headers=headers,
106
+ timeout=30,
107
+ )
108
+ data = resp.json()
109
+
110
+ if data.get("code") != 0:
111
+ raise Exception(f"Get PoW failed: {data.get('msg')}")
112
+
113
+ return data.get("data", {}).get("biz_data", {}).get("challenge", {})
114
+
115
+ def _compute_pow(self, challenge: dict) -> str:
116
+ prefix = challenge.get("prefix", "")
117
+ target = challenge.get("target", "")
118
+ expire_at = challenge.get("expire_at", 0)
119
+
120
+ nonce = 0
121
+ while nonce < 10000000:
122
+ test = f"{prefix}{nonce}"
123
+ hash_val = hashlib.sha256(test.encode()).hexdigest()
124
+ if hash_val.startswith("0" * len(target)):
125
+ break
126
+ nonce += 1
127
+
128
+ return json.dumps({
129
+ "prefix": prefix,
130
+ "nonce": nonce,
131
+ "expire_at": expire_at,
132
+ "target": target,
133
+ })
134
+
135
+ async def send_message(
136
+ self,
137
+ prompt: str,
138
+ model: str = "deepseek-chat",
139
+ stream: bool = False,
140
+ timeout: int = 120,
141
+ ) -> str:
142
+ if not self._token:
143
+ await self.login()
144
+
145
+ session_id = await self.create_session()
146
+
147
+ pow_challenge = await self.get_pow()
148
+ pow_header = self._compute_pow(pow_challenge)
149
+
150
+ client = await self._get_client()
151
+ headers = self._auth_headers()
152
+ headers["x-ds-pow-response"] = pow_header
153
+
154
+ payload = {
155
+ "chat_session_id": session_id,
156
+ "parent_message_id": None,
157
+ "prompt": prompt,
158
+ "ref_file_ids": [],
159
+ "thinking_enabled": True,
160
+ "search_enabled": "search" in model.lower(),
161
+ }
162
+
163
+ if stream:
164
+ return self._stream_completion(client, headers, payload, timeout)
165
+ else:
166
+ return await self._sync_completion(client, headers, payload, timeout)
167
+
168
+ async def _sync_completion(
169
+ self,
170
+ client: httpx.AsyncClient,
171
+ headers: dict,
172
+ payload: dict,
173
+ timeout: int,
174
+ ) -> str:
175
+ resp = await client.post(
176
+ self.COMPLETION_URL,
177
+ json=payload,
178
+ headers=headers,
179
+ timeout=timeout,
180
+ )
181
+
182
+ # 处理 SSE 响应
183
+ full_text = ""
184
+ for line in resp.text.split("\n"):
185
+ if line.startswith("data:"):
186
+ data_str = line[5:].strip()
187
+ if data_str == "[DONE]":
188
+ continue
189
+ try:
190
+ data = json.loads(data_str)
191
+ msg = data.get("message", {})
192
+ content = msg.get("content", "")
193
+ if content:
194
+ full_text += content
195
+ except json.JSONDecodeError:
196
+ continue
197
+
198
+ return full_text
199
+
200
+ async def _stream_completion(
201
+ self,
202
+ client: httpx.AsyncClient,
203
+ headers: dict,
204
+ payload: dict,
205
+ timeout: int,
206
+ ) -> AsyncGenerator[str, None]:
207
+ async with client.stream(
208
+ "POST",
209
+ self.COMPLETION_URL,
210
+ json=payload,
211
+ headers=headers,
212
+ timeout=timeout,
213
+ ) as resp:
214
+ async for line in resp.aiter_lines():
215
+ if line.startswith("data:"):
216
+ data_str = line[5:].strip()
217
+ if data_str == "[DONE]":
218
+ return
219
+ try:
220
+ data = json.loads(data_str)
221
+ msg = data.get("message", {})
222
+ content = msg.get("content", "")
223
+ if content:
224
+ yield content
225
+ except json.JSONDecodeError:
226
+ continue
227
+
228
+ async def delete_session(self, session_id: str):
229
+ if not self._token:
230
+ return
231
+
232
+ client = await self._get_client()
233
+ headers = self._auth_headers()
234
+
235
+ await client.post(
236
+ self.DELETE_SESSION_URL,
237
+ json={"chat_session_id": session_id},
238
+ headers=headers,
239
+ timeout=30,
240
+ )
241
+
242
+ def _base_headers(self) -> dict:
243
+ return {
244
+ "Content-Type": "application/json",
245
+ "User-Agent": "DeepSeek/3.2.1 Android/35",
246
+ "x-client-platform": "android",
247
+ "x-client-version": "3.2.1",
248
+ "x-client-locale": "zh_CN",
249
+ "Accept-Encoding": "gzip, deflate, br",
250
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
251
+ }
252
+
253
+ def _auth_headers(self) -> dict:
254
+ headers = self._base_headers()
255
+ if self._token:
256
+ headers["authorization"] = f"Bearer {self._token}"
257
+ return headers
258
+
259
+ async def close(self):
260
+ if self._client:
261
+ await self._client.aclose()
deepseek_browser.py CHANGED
@@ -1,14 +1,19 @@
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,
@@ -29,10 +34,15 @@ class DeepSeekBrowser:
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,
@@ -43,60 +53,67 @@ class DeepSeekBrowser:
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
@@ -180,35 +197,37 @@ class DeepSeekBrowser:
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
@@ -250,37 +269,39 @@ class DeepSeekBrowser:
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):]
 
1
  import asyncio
2
  import random
3
  import time
4
+ import uuid
5
  from pathlib import Path
6
  from typing import AsyncGenerator, Optional
7
 
8
+ import httpx
9
  from cloakbrowser import launch_persistent_context_async
10
 
11
 
12
  class DeepSeekBrowser:
13
  DEEPSEEK_URL = "https://chat.deepseek.com"
14
+ LOGIN_URL = "https://chat.deepseek.com/api/v0/users/login"
15
+ CREATE_SESSION_URL = "https://chat.deepseek.com/api/v0/chat_session/create"
16
+ COMPLETION_URL = "https://chat.deepseek.com/api/v0/chat/completion"
17
 
18
  def __init__(
19
  self,
 
34
  self.page = None
35
  self._logged_in = False
36
  self._ready = False
37
+ self._token = None
38
+ self._session_id = None
39
 
40
  async def start(self):
41
  self.profile_dir.mkdir(parents=True, exist_ok=True)
42
 
43
+ # 先用 API 登录获取 token
44
+ await self._login_via_api()
45
+
46
  self.context = await launch_persistent_context_async(
47
  user_data_dir=str(self.profile_dir),
48
  headless=self.headless,
 
53
  )
54
 
55
  self.page = await self.context.new_page()
56
+
57
+ # 设置 token cookie
58
+ if self._token:
59
+ await self.context.add_cookies([{
60
+ "name": "token",
61
+ "value": self._token,
62
+ "domain": ".deepseek.com",
63
+ "path": "/",
64
+ }])
65
 
66
+ await self.page.goto(self.DEEPSEEK_URL, timeout=60000)
67
+ await asyncio.sleep(3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
+ # 检查是否登录成功
70
+ if '/sign_in' in self.page.url:
71
+ # 如果 cookie 方式失败,尝试通过 JS 注入 token
72
+ await self.page.evaluate(f"localStorage.setItem('token', '{self._token}')")
73
+ await self.page.reload()
74
  await asyncio.sleep(3)
 
 
 
75
 
76
+ if '/sign_in' not in self.page.url:
 
77
  self._logged_in = True
78
  self._ready = True
79
+ else:
80
+ raise Exception("Login failed - still on sign_in page")
81
+
82
+ async def _login_via_api(self):
83
+ """通过 DeepSeek API 登录获取 token"""
84
+ async with httpx.AsyncClient() as client:
85
+ device_id = str(uuid.uuid4())
86
+ payload = {
87
+ "email": self.email,
88
+ "password": self.password,
89
+ "device_id": device_id,
90
+ "os": "android",
91
+ }
92
+
93
+ headers = {
94
+ "Content-Type": "application/json",
95
+ "User-Agent": "DeepSeek-Android/2.0",
96
+ }
97
+
98
+ resp = await client.post(self.LOGIN_URL, json=payload, headers=headers, timeout=30)
99
+ data = resp.json()
100
+
101
+ code = data.get("code", -1)
102
+ if code != 0:
103
+ msg = data.get("msg", "Unknown error")
104
+ raise Exception(f"API login failed: {msg}")
105
+
106
+ biz_data = data.get("data", {})
107
+ biz_code = biz_data.get("biz_code", -1)
108
+ if biz_code != 0:
109
+ biz_msg = biz_data.get("biz_msg", "Unknown error")
110
+ raise Exception(f"API login failed: {biz_msg}")
111
+
112
+ user = biz_data.get("biz_data", {}).get("user", {})
113
+ self._token = user.get("token", "")
114
+
115
+ if not self._token:
116
+ raise Exception("No token received from API")
117
 
118
  async def _human_delay(self, min_ms: int = 300, max_ms: int = 1500):
119
  delay = random.uniform(min_ms, max_ms) / 1000
 
197
  last_text = ""
198
  stable_count = 0
199
 
200
+ skip_phrases = ['深度思考', '智能搜索', '快速模式', '专家模式', '内容由 AI 生成', '开启新对话', '暂无历史对话']
201
 
202
  while time.time() < deadline:
203
  try:
204
+ try:
205
+ response_elements = await self.page.query_selector_all('.ds-markdown--block')
206
+ if response_elements:
207
+ last_response = response_elements[-1]
208
+ current_text = await last_response.inner_text()
209
+ current_text = current_text.strip()
210
+ else:
211
+ main_content = await self.page.query_selector('main, .chat-container, [class*="chat"]')
212
+ if main_content:
213
+ current_text = await main_content.inner_text()
214
+ else:
215
+ current_text = await self.page.inner_text('body')
216
+ except Exception:
217
+ current_text = await self.page.inner_text('body')
218
 
219
+ lines = current_text.split('\n')
220
+ filtered_lines = []
221
  for line in lines:
222
  line = line.strip()
223
  if not line:
224
  continue
 
 
 
 
225
  if any(phrase in line for phrase in skip_phrases):
226
  continue
227
+ filtered_lines.append(line)
228
 
229
+ if filtered_lines:
230
+ current_text = '\n'.join(filtered_lines)
 
 
 
 
 
 
231
 
232
  if current_text != last_text:
233
  last_text = current_text
 
269
  last_text = ""
270
  stable_count = 0
271
 
272
+ skip_phrases = ['深度思考', '智能搜索', '快速模式', '专家模式', '内容由 AI 生成', '开启新对话', '暂无历史对话']
273
 
274
  await asyncio.sleep(3)
275
 
276
  while time.time() < deadline:
277
  try:
278
+ try:
279
+ response_elements = await self.page.query_selector_all('.ds-markdown--block')
280
+ if response_elements:
281
+ last_response = response_elements[-1]
282
+ current_text = await last_response.inner_text()
283
+ current_text = current_text.strip()
284
+ else:
285
+ main_content = await self.page.query_selector('main, .chat-container, [class*="chat"]')
286
+ if main_content:
287
+ current_text = await main_content.inner_text()
288
+ else:
289
+ current_text = await self.page.inner_text('body')
290
+ except Exception:
291
+ current_text = await self.page.inner_text('body')
292
+
293
+ lines = current_text.split('\n')
294
+ filtered_lines = []
295
  for line in lines:
296
  line = line.strip()
297
  if not line:
298
  continue
 
 
 
 
299
  if any(phrase in line for phrase in skip_phrases):
300
  continue
301
+ filtered_lines.append(line)
302
 
303
+ if filtered_lines:
304
+ current_text = '\n'.join(filtered_lines)
 
 
 
 
 
 
305
 
306
  if current_text != last_text:
307
  new_chunk = current_text[len(last_text):]
main.py CHANGED
@@ -105,13 +105,13 @@ async def chat_completions(
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",
@@ -152,7 +152,7 @@ async def chat_completions(
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
 
@@ -211,12 +211,12 @@ async def anthropic_messages(request: Request, authorization: str = Header(...))
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,
@@ -235,7 +235,7 @@ async def anthropic_messages(request: Request, authorization: str = Header(...))
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
 
@@ -272,9 +272,9 @@ async def gemini_generate(model: str, request: Request, authorization: str = Hea
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
 
@@ -315,11 +315,11 @@ async def gemini_stream_generate(model: str, request: Request, authorization: st
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
  {
@@ -442,12 +442,39 @@ async def list_accounts(admin_key: str = Header(...)):
442
  accounts.append({
443
  "email": email,
444
  "name": acc.name,
445
- "in_use": acc.in_use,
 
 
446
  "logged_in": acc.logged_in,
447
  "error_count": acc.error_count,
448
  })
449
 
450
- return {"accounts": accounts, "total": len(accounts)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
 
452
 
453
  @app.on_event("startup")
 
105
  account = await manager.acquire()
106
 
107
  try:
108
+ api = await manager.get_api_with_retry(account)
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 api.send_message(prompt, model=request.model, stream=True, timeout=120):
115
  data = {
116
  "id": chunk_id,
117
  "object": "chat.completion.chunk",
 
152
  media_type="text/event-stream",
153
  )
154
 
155
+ response_text = await api.send_message(prompt, model=request.model, stream=False, timeout=120)
156
 
157
  await manager.release(account)
158
 
 
211
  account = await manager.acquire()
212
 
213
  try:
214
+ api = await manager.get_api_with_retry(account)
215
 
216
  if stream:
217
  async def stream_with_cleanup():
218
  try:
219
+ async for chunk in api.send_message(prompt, model=model, stream=True, timeout=120):
220
  data = {
221
  "type": "content_block_delta",
222
  "index": 0,
 
235
  media_type="text/event-stream",
236
  )
237
 
238
+ response_text = await api.send_message(prompt, model=model, stream=False, timeout=120)
239
 
240
  await manager.release(account)
241
 
 
272
  account = await manager.acquire()
273
 
274
  try:
275
+ api = await manager.get_api_with_retry(account)
276
 
277
+ response_text = await api.send_message(prompt, model=model, stream=False, timeout=120)
278
 
279
  await manager.release(account)
280
 
 
315
  account = await manager.acquire()
316
 
317
  try:
318
+ api = await manager.get_api_with_retry(account)
319
 
320
  async def stream_with_cleanup():
321
  try:
322
+ async for chunk in api.send_message(prompt, model=model, stream=True, timeout=120):
323
  data = {
324
  "candidates": [
325
  {
 
442
  accounts.append({
443
  "email": email,
444
  "name": acc.name,
445
+ "proxy": acc.proxy,
446
+ "in_use_count": acc.in_use_count,
447
+ "max_concurrent": acc.max_concurrent,
448
  "logged_in": acc.logged_in,
449
  "error_count": acc.error_count,
450
  })
451
 
452
+ return {
453
+ "accounts": accounts,
454
+ "total": len(accounts),
455
+ "max_concurrent_per_account": config.browser.max_concurrent_per_account,
456
+ "default_proxy": config.default_proxy,
457
+ }
458
+
459
+
460
+ @app.get("/admin/config")
461
+ async def get_config(admin_key: str = Header(...)):
462
+ if admin_key != config.server.admin_key:
463
+ raise HTTPException(status_code=401, detail="Invalid admin key")
464
+
465
+ return {
466
+ "server": {
467
+ "host": config.server.host,
468
+ "port": config.server.port,
469
+ },
470
+ "browser": {
471
+ "headless": config.browser.headless,
472
+ "max_concurrent_per_account": config.browser.max_concurrent_per_account,
473
+ "timeout": config.browser.timeout,
474
+ },
475
+ "default_proxy": config.default_proxy,
476
+ "account_count": len(manager.accounts),
477
+ }
478
 
479
 
480
  @app.on_event("startup")
proxy.py ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import time
3
+ import uuid
4
+ from typing import Optional
5
+
6
+ import httpx
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
+ app = FastAPI(title="DS2API Browser Proxy")
13
+
14
+ app.add_middleware(
15
+ CORSMiddleware,
16
+ allow_origins=["*"],
17
+ allow_credentials=True,
18
+ allow_methods=["*"],
19
+ allow_headers=["*"],
20
+ )
21
+
22
+ DS2API_URL = "http://127.0.0.1:5001"
23
+ API_KEYS = ["sk-default", "sk-test123456"]
24
+ ADMIN_KEY = "admin"
25
+
26
+
27
+ class Message(BaseModel):
28
+ role: str
29
+ content: str
30
+
31
+
32
+ class ChatCompletionRequest(BaseModel):
33
+ model: str
34
+ messages: list[Message]
35
+ stream: bool = False
36
+ temperature: Optional[float] = None
37
+ max_tokens: Optional[int] = None
38
+
39
+
40
+ def verify_api_key(authorization: Optional[str] = Header(None)) -> str:
41
+ if not authorization:
42
+ raise HTTPException(status_code=401, detail="Missing API key")
43
+
44
+ token = authorization.replace("Bearer ", "").strip()
45
+ if token not in API_KEYS:
46
+ raise HTTPException(status_code=401, detail="Invalid API key")
47
+
48
+ return token
49
+
50
+
51
+ @app.get("/v1/models")
52
+ async def list_models(authorization: str = Header(...)):
53
+ verify_api_key(authorization)
54
+
55
+ async with httpx.AsyncClient() as client:
56
+ resp = await client.get(f"{DS2API_URL}/v1/models", headers={"Authorization": f"Bearer sk-test123456"})
57
+ return resp.json()
58
+
59
+
60
+ @app.get("/v1/models/{model_id}")
61
+ async def get_model(model_id: str, authorization: str = Header(...)):
62
+ verify_api_key(authorization)
63
+
64
+ async with httpx.AsyncClient() as client:
65
+ resp = await client.get(f"{DS2API_URL}/v1/models/{model_id}", headers={"Authorization": f"Bearer sk-test123456"})
66
+ return resp.json()
67
+
68
+
69
+ @app.post("/v1/chat/completions")
70
+ async def chat_completions(
71
+ request: ChatCompletionRequest,
72
+ authorization: str = Header(...),
73
+ ):
74
+ verify_api_key(authorization)
75
+
76
+ if not request.messages:
77
+ raise HTTPException(status_code=400, detail="No messages provided")
78
+
79
+ async with httpx.AsyncClient() as client:
80
+ if request.stream:
81
+ async def stream_with_cleanup():
82
+ async with httpx.AsyncClient() as stream_client:
83
+ async with stream_client.stream(
84
+ "POST",
85
+ f"{DS2API_URL}/v1/chat/completions",
86
+ json=request.model_dump(),
87
+ headers={"Authorization": "Bearer sk-test123456"},
88
+ timeout=120,
89
+ ) as resp:
90
+ async for line in resp.aiter_lines():
91
+ yield line + "\n"
92
+
93
+ return StreamingResponse(
94
+ stream_with_cleanup(),
95
+ media_type="text/event-stream",
96
+ )
97
+
98
+ resp = await client.post(
99
+ f"{DS2API_URL}/v1/chat/completions",
100
+ json=request.model_dump(),
101
+ headers={"Authorization": "Bearer sk-test123456"},
102
+ timeout=120,
103
+ )
104
+ return resp.json()
105
+
106
+
107
+ @app.get("/anthropic/v1/models")
108
+ async def anthropic_models(authorization: str = Header(...)):
109
+ verify_api_key(authorization)
110
+
111
+ return {
112
+ "data": [
113
+ {"id": "claude-sonnet-4-6", "object": "model", "created": int(time.time()), "owned_by": "anthropic"},
114
+ {"id": "claude-opus-4-6", "object": "model", "created": int(time.time()), "owned_by": "anthropic"},
115
+ {"id": "claude-haiku-4-5", "object": "model", "created": int(time.time()), "owned_by": "anthropic"},
116
+ ],
117
+ "object": "list",
118
+ }
119
+
120
+
121
+ @app.post("/anthropic/v1/messages")
122
+ async def anthropic_messages(request: Request, authorization: str = Header(...)):
123
+ verify_api_key(authorization)
124
+
125
+ body = await request.json()
126
+ messages = body.get("messages", [])
127
+ model = body.get("model", "claude-sonnet-4-6")
128
+ stream = body.get("stream", False)
129
+
130
+ if not messages:
131
+ raise HTTPException(status_code=400, detail="No messages provided")
132
+
133
+ prompt = messages[-1].get("content", "")
134
+
135
+ async with httpx.AsyncClient() as client:
136
+ if stream:
137
+ async def stream_with_cleanup():
138
+ async with httpx.AsyncClient() as stream_client:
139
+ async with stream_client.stream(
140
+ "POST",
141
+ f"{DS2API_URL}/v1/chat/completions",
142
+ json={"model": "deepseek-v4-flash", "messages": [{"role": "user", "content": prompt}], "stream": True},
143
+ headers={"Authorization": "Bearer sk-test123456"},
144
+ timeout=120,
145
+ ) as resp:
146
+ async for line in resp.aiter_lines():
147
+ if line.startswith("data: "):
148
+ data_str = line[6:].strip()
149
+ if data_str == "[DONE]":
150
+ continue
151
+ try:
152
+ data = json.loads(data_str)
153
+ content = data.get("choices", [{}])[0].get("delta", {}).get("content", "")
154
+ if content:
155
+ yield f"event: content_block_delta\ndata: {json.dumps({'type': 'content_block_delta', 'index': 0, 'delta': {'type': 'text_delta', 'text': content}})}\n\n"
156
+ except json.JSONDecodeError:
157
+ pass
158
+
159
+ yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n"
160
+
161
+ return StreamingResponse(
162
+ stream_with_cleanup(),
163
+ media_type="text/event-stream",
164
+ )
165
+
166
+ resp = await client.post(
167
+ f"{DS2API_URL}/v1/chat/completions",
168
+ json={"model": "deepseek-v4-flash", "messages": [{"role": "user", "content": prompt}], "stream": False},
169
+ headers={"Authorization": "Bearer sk-test123456"},
170
+ timeout=120,
171
+ )
172
+ data = resp.json()
173
+ content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
174
+
175
+ return {
176
+ "id": f"msg_{uuid.uuid4().hex[:8]}",
177
+ "type": "message",
178
+ "role": "assistant",
179
+ "model": model,
180
+ "content": [{"type": "text", "text": content}],
181
+ "stop_reason": "end_turn",
182
+ "usage": {
183
+ "input_tokens": len(prompt.split()),
184
+ "output_tokens": len(content.split()),
185
+ },
186
+ }
187
+
188
+
189
+ @app.post("/v1beta/models/{model}:generateContent")
190
+ async def gemini_generate(model: str, request: Request, authorization: str = Header(...)):
191
+ verify_api_key(authorization)
192
+
193
+ body = await request.json()
194
+ contents = body.get("contents", [])
195
+
196
+ if not contents:
197
+ raise HTTPException(status_code=400, detail="No contents provided")
198
+
199
+ prompt = contents[-1].get("parts", [{}])[0].get("text", "")
200
+
201
+ async with httpx.AsyncClient() as client:
202
+ resp = await client.post(
203
+ f"{DS2API_URL}/v1/chat/completions",
204
+ json={"model": "deepseek-v4-flash", "messages": [{"role": "user", "content": prompt}], "stream": False},
205
+ headers={"Authorization": "Bearer sk-test123456"},
206
+ timeout=120,
207
+ )
208
+ data = resp.json()
209
+ content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
210
+
211
+ return {
212
+ "candidates": [
213
+ {
214
+ "content": {
215
+ "parts": [{"text": content}],
216
+ "role": "model",
217
+ },
218
+ "finishReason": "STOP",
219
+ }
220
+ ],
221
+ "usageMetadata": {
222
+ "promptTokenCount": len(prompt.split()),
223
+ "candidatesTokenCount": len(content.split()),
224
+ "totalTokenCount": len(prompt.split()) + len(content.split()),
225
+ },
226
+ }
227
+
228
+
229
+ @app.post("/v1beta/models/{model}:streamGenerateContent")
230
+ async def gemini_stream_generate(model: str, request: Request, authorization: str = Header(...)):
231
+ verify_api_key(authorization)
232
+
233
+ body = await request.json()
234
+ contents = body.get("contents", [])
235
+
236
+ if not contents:
237
+ raise HTTPException(status_code=400, detail="No contents provided")
238
+
239
+ prompt = contents[-1].get("parts", [{}])[0].get("text", "")
240
+
241
+ async def stream_with_cleanup():
242
+ async with httpx.AsyncClient() as stream_client:
243
+ async with stream_client.stream(
244
+ "POST",
245
+ f"{DS2API_URL}/v1/chat/completions",
246
+ json={"model": "deepseek-v4-flash", "messages": [{"role": "user", "content": prompt}], "stream": True},
247
+ headers={"Authorization": "Bearer sk-test123456"},
248
+ timeout=120,
249
+ ) as resp:
250
+ async for line in resp.aiter_lines():
251
+ if line.startswith("data: "):
252
+ data_str = line[6:].strip()
253
+ if data_str == "[DONE]":
254
+ continue
255
+ try:
256
+ data = json.loads(data_str)
257
+ content = data.get("choices", [{}])[0].get("delta", {}).get("content", "")
258
+ if content:
259
+ yield f"data: {json.dumps({'candidates': [{'content': {'parts': [{'text': content}], 'role': 'model'}}]})}\n\n"
260
+ except json.JSONDecodeError:
261
+ pass
262
+
263
+ yield f"data: {json.dumps({'candidates': [{'content': {'parts': [], 'role': 'model'}, 'finishReason': 'STOP'}], 'usageMetadata': {'promptTokenCount': 0, 'candidatesTokenCount': 0, 'totalTokenCount': 0}})}\n\n"
264
+
265
+ return StreamingResponse(
266
+ stream_with_cleanup(),
267
+ media_type="text/event-stream",
268
+ )
269
+
270
+
271
+ @app.get("/api/version")
272
+ async def ollama_version():
273
+ return {"version": "0.1.0"}
274
+
275
+
276
+ @app.get("/api/tags")
277
+ async def ollama_tags():
278
+ return {
279
+ "models": [
280
+ {"name": "deepseek-chat", "model": "deepseek-chat"},
281
+ {"name": "deepseek-reasoner", "model": "deepseek-reasoner"},
282
+ ]
283
+ }
284
+
285
+
286
+ @app.post("/api/show")
287
+ async def ollama_show(request: Request):
288
+ body = await request.json()
289
+ model = body.get("model", "deepseek-chat")
290
+
291
+ return {
292
+ "id": model,
293
+ "capabilities": ["tools", "thinking"],
294
+ }
295
+
296
+
297
+ @app.get("/healthz")
298
+ async def healthz():
299
+ return {"status": "ok"}
300
+
301
+
302
+ @app.get("/readyz")
303
+ async def readyz():
304
+ return {"status": "ok", "accounts": {"total": 1, "in_use": 0, "available": 1}}
305
+
306
+
307
+ @app.get("/admin/stats")
308
+ async def admin_stats(admin_key: str = Header(...)):
309
+ if admin_key != ADMIN_KEY:
310
+ raise HTTPException(status_code=401, detail="Invalid admin key")
311
+
312
+ return {"total": 1, "in_use": 0, "available": 1, "logged_in": 1, "queue_size": 0}
313
+
314
+
315
+ @app.get("/admin/config")
316
+ async def get_config(admin_key: str = Header(...)):
317
+ if admin_key != ADMIN_KEY:
318
+ raise HTTPException(status_code=401, detail="Invalid admin key")
319
+
320
+ return {
321
+ "server": {"host": "0.0.0.0", "port": 5002},
322
+ "browser": {"headless": True, "max_concurrent_per_account": 1, "timeout": 60000},
323
+ "default_proxy": None,
324
+ "account_count": 1,
325
+ }
326
+
327
+
328
+ def main():
329
+ import uvicorn
330
+
331
+ uvicorn.run(
332
+ app,
333
+ host="0.0.0.0",
334
+ port=5002,
335
+ )
336
+
337
+
338
+ if __name__ == "__main__":
339
+ main()