nacho commited on
Commit
3a73b53
·
2 Parent(s): 5cba2672940fed

merge: feat/admin-panel into master

Browse files

- Add Chinese admin panel with Ghost Terminal design
- Add mute detection (shows mute status and expiry)
- Add dual-column layout with built-in API testing
- Keep browser automation with CloakBrowser

Files changed (5) hide show
  1. account_manager.py +51 -31
  2. config.py +1 -1
  3. deepseek_browser.py +212 -122
  4. main.py +329 -260
  5. test.html +0 -306
account_manager.py CHANGED
@@ -3,7 +3,7 @@ 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,18 +12,19 @@ class Account:
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
 
21
 
22
  class AccountManager:
23
- def __init__(self, max_concurrent_per_account: int = 3):
24
  self.accounts: Dict[str, Account] = {}
25
  self.queue: deque = deque()
26
- self.max_concurrent_per_account = max_concurrent_per_account
27
  self._lock = asyncio.Lock()
28
 
29
  def add_account(self, email: str, password: str, name: str = "", proxy: Optional[str] = None):
@@ -32,14 +33,13 @@ class AccountManager:
32
  password=password,
33
  name=name,
34
  proxy=proxy,
35
- max_concurrent=self.max_concurrent_per_account,
36
  )
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()
@@ -53,15 +53,15 @@ class AccountManager:
53
 
54
  async with self._lock:
55
  for account in self.accounts.values():
56
- if account.in_use_count < account.max_concurrent and account.error_count < 3:
57
- account.in_use_count += 1
58
  return account
59
 
60
  raise RuntimeError("No account available")
61
 
62
  async def release(self, account: Account):
63
  async with self._lock:
64
- account.in_use_count = max(0, account.in_use_count - 1)
65
  if self.queue:
66
  event = self.queue.popleft()
67
  event.set()
@@ -69,53 +69,73 @@ class AccountManager:
69
  async def mark_error(self, account: Account):
70
  async with self._lock:
71
  account.error_count += 1
72
- account.in_use_count = max(0, account.in_use_count - 1)
73
  if self.queue:
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:
110
  total = len(self.accounts)
111
- in_use = sum(a.in_use_count for a in self.accounts.values())
112
- available = sum(1 for a in self.accounts.values() if a.in_use_count < a.max_concurrent and a.error_count < 3)
113
  logged_in = sum(1 for a in self.accounts.values() if a.logged_in)
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  return {
115
  "total": total,
116
  "in_use": in_use,
117
  "available": available,
118
  "logged_in": logged_in,
 
119
  "queue_size": len(self.queue),
120
- "max_concurrent_per_account": self.max_concurrent_per_account,
121
  }
 
3
  from dataclasses import dataclass, field
4
  from typing import Dict, Optional
5
 
6
+ from deepseek_browser import DeepSeekBrowser
7
 
8
 
9
  @dataclass
 
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
+ is_muted: bool = False
20
+ muted_until: str = ""
21
 
22
 
23
  class AccountManager:
24
+ def __init__(self, max_inflight: int = 1):
25
  self.accounts: Dict[str, Account] = {}
26
  self.queue: deque = deque()
27
+ self.max_inflight = max_inflight
28
  self._lock = asyncio.Lock()
29
 
30
  def add_account(self, email: str, password: str, name: str = "", proxy: Optional[str] = None):
 
33
  password=password,
34
  name=name,
35
  proxy=proxy,
 
36
  )
37
 
38
  async def acquire(self) -> Account:
39
  async with self._lock:
40
  for account in self.accounts.values():
41
+ if not account.in_use and account.error_count < 3:
42
+ account.in_use = True
43
  return account
44
 
45
  return await self._wait_for_account()
 
53
 
54
  async with self._lock:
55
  for account in self.accounts.values():
56
+ if not account.in_use and account.error_count < 3:
57
+ account.in_use = True
58
  return account
59
 
60
  raise RuntimeError("No account available")
61
 
62
  async def release(self, account: Account):
63
  async with self._lock:
64
+ account.in_use = False
65
  if self.queue:
66
  event = self.queue.popleft()
67
  event.set()
 
69
  async def mark_error(self, account: Account):
70
  async with self._lock:
71
  account.error_count += 1
72
+ account.in_use = False
73
  if self.queue:
74
  event = self.queue.popleft()
75
  event.set()
76
 
77
+ async def get_or_create_browser(self, account: Account, headless: bool = True) -> DeepSeekBrowser:
78
  try:
79
+ if account.browser is None:
80
+ account.browser = DeepSeekBrowser(
81
  email=account.email,
82
  password=account.password,
83
+ profile_dir="./profiles",
84
+ headless=headless,
85
+ humanize=True,
86
  proxy=account.proxy,
87
  )
88
+ await account.browser.start()
89
  account.logged_in = True
90
+ # Check mute status
91
+ account.is_muted = account.browser.is_muted()
92
+ account.muted_until = account.browser.muted_until()
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:
116
  total = len(self.accounts)
117
+ in_use = sum(1 for a in self.accounts.values() if a.in_use)
118
+ available = sum(1 for a in self.accounts.values() if not a.in_use and a.error_count < 3)
119
  logged_in = sum(1 for a in self.accounts.values() if a.logged_in)
120
+ muted = sum(1 for a in self.accounts.values() if a.is_muted)
121
+ accounts_list = [
122
+ {
123
+ "email": a.email,
124
+ "name": a.name,
125
+ "in_use": a.in_use,
126
+ "logged_in": a.logged_in,
127
+ "is_muted": a.is_muted,
128
+ "muted_until": a.muted_until,
129
+ "error_count": a.error_count,
130
+ }
131
+ for a in self.accounts.values()
132
+ ]
133
  return {
134
  "total": total,
135
  "in_use": in_use,
136
  "available": available,
137
  "logged_in": logged_in,
138
+ "muted": muted,
139
  "queue_size": len(self.queue),
140
+ "accounts": accounts_list,
141
  }
config.py CHANGED
@@ -43,7 +43,7 @@ class Config:
43
  account_str = os.getenv("DS2API_ACCOUNTS", "")
44
  if account_str:
45
  for acc in account_str.split(";"):
46
- parts = acc.split(":")
47
  if len(parts) >= 2:
48
  accounts.append(AccountConfig(
49
  email=parts[0],
 
43
  account_str = os.getenv("DS2API_ACCOUNTS", "")
44
  if account_str:
45
  for acc in account_str.split(";"):
46
+ parts = acc.split(":", 3)
47
  if len(parts) >= 2:
48
  accounts.append(AccountConfig(
49
  email=parts[0],
deepseek_browser.py CHANGED
@@ -1,19 +1,14 @@
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,15 +29,10 @@ class DeepSeekBrowser:
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,67 +43,95 @@ class DeepSeekBrowser:
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
@@ -130,26 +148,102 @@ class DeepSeekBrowser:
130
 
131
  async def delete_chat(self):
132
  try:
133
- more_btn = self.page.locator('button:has-text("更多"), .ds-icon-button:has-text("...")').first
134
- if await more_btn.count() > 0:
135
- await more_btn.click()
136
- await asyncio.sleep(0.5)
137
-
138
- delete_btn = self.page.locator('button:has-text("删除"), div:has-text("删除对话")').first
139
- if await delete_btn.count() > 0:
140
- await delete_btn.click()
141
- await asyncio.sleep(0.5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
- confirm_btn = self.page.locator('button:has-text("确认"), button:has-text("删除")').last
144
- if await confirm_btn.count() > 0:
145
- await confirm_btn.click()
146
- await asyncio.sleep(1)
147
- except Exception:
148
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
 
150
  async def switch_model(self, model: str):
151
  try:
152
- if 'reasoner' in model or 'thinking' in model:
153
  thinking_btn = self.page.locator('button:has-text("深度思考"), div:has-text("深度思考")').first
154
  if await thinking_btn.count() > 0:
155
  await thinking_btn.click()
@@ -197,37 +291,35 @@ class DeepSeekBrowser:
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,39 +361,37 @@ class DeepSeekBrowser:
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):]
 
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
  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
  )
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
+ # Check if account is muted after login
65
+ if self._logged_in:
66
+ await self._check_mute()
67
+
68
+ async def _check_mute(self):
69
+ """Check if account is muted and extract mute expiry."""
70
+ try:
71
+ muted, until = await self.page.evaluate("""() => {
72
+ const text = document.body.innerText || '';
73
+ // Match: 禁言至 YYYY年M月D日 HH:MM or 禁言至 YYYY-MM-DD HH:MM
74
+ const match = text.match(/禁言至\\s*(\\d{4}[-年]\\d{1,2}[-月]\\d{1,2}[日]?\\s*\\d{1,2}:\\d{2})/);
75
+ if (match) return [true, match[1]];
76
+ if (text.includes('禁言')) return [true, ''];
77
+ return [false, ''];
78
+ }""")
79
+ self._is_muted = muted
80
+ self._muted_until = until
81
+ if muted:
82
+ print(f"[mute] {self.email} is muted until {until}")
83
+ except Exception:
84
+ self._is_muted = False
85
+ self._muted_until = ""
86
+
87
+ def is_muted(self) -> bool:
88
+ return getattr(self, '_is_muted', False)
89
+
90
+ def muted_until(self) -> str:
91
+ return getattr(self, '_muted_until', "")
92
+
93
+ async def _auto_login(self):
94
+ print(f"Logging in as {self.email}...")
95
+
96
+ try:
97
+ email_input = self.page.locator('input[placeholder*="邮箱"], input[placeholder*="手机"], input[placeholder*="Email"], input[placeholder*="email"], input[type="text"]').first
98
+ await email_input.wait_for(state="visible", timeout=10000)
99
+ await email_input.fill(self.email)
100
+ await asyncio.sleep(0.5)
101
+ except Exception as e:
102
+ # Take screenshot to debug
103
+ try:
104
+ await self.page.screenshot(path=f"/tmp/login_fail_{self.email.replace('@','_at_')}.png")
105
+ print(f"Screenshot saved to /tmp/login_fail_{self.email.replace('@','_at_')}.png")
106
+ except Exception:
107
+ pass
108
+ print(f"Email input error: {e}")
109
+ raise
110
+
111
+ try:
112
+ password_input = self.page.locator('input[type="password"]').first
113
+ await password_input.wait_for(state="visible", timeout=5000)
114
+ await password_input.fill(self.password)
115
+ await asyncio.sleep(0.5)
116
+ except Exception as e:
117
+ print(f"Password input error: {e}")
118
+ raise
119
+
120
+ try:
121
+ login_button = self.page.locator('button:has-text("登录")').first
122
+ await login_button.click()
123
  await asyncio.sleep(3)
124
+ except Exception as e:
125
+ print(f"Login button error: {e}")
126
+ raise
127
 
128
+ try:
129
+ await self.page.wait_for_selector('textarea', timeout=30000)
130
  self._logged_in = True
131
  self._ready = True
132
+ print("Login successful!")
133
+ except Exception:
134
+ raise Exception("Login failed")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
  async def _human_delay(self, min_ms: int = 300, max_ms: int = 1500):
137
  delay = random.uniform(min_ms, max_ms) / 1000
 
148
 
149
  async def delete_chat(self):
150
  try:
151
+ # Find the sidebar and active conversation
152
+ chat_list = self.page.locator(
153
+ 'nav, aside, [class*="sidebar"], [class*="Sidebar"], div:has-text("开启新对话")'
154
+ ).first
155
+ chat_list_count = await chat_list.count()
156
+ if chat_list_count == 0:
157
+ print(f"[delete_chat] no sidebar")
158
+ return
159
+
160
+ active_item = chat_list.locator(
161
+ '[class*="active"], [class*="selected"], [class*="current"]'
162
+ ).first
163
+ active_count = await active_item.count()
164
+ if active_count == 0:
165
+ # No active item yet (first chat), skip deletion
166
+ print(f"[delete_chat] no active item, skipping")
167
+ return
168
+
169
+ # Get bounding box and click near right edge where "..." should be
170
+ box = await active_item.bounding_box()
171
+ if not box:
172
+ print(f"[delete_chat] no bbox")
173
+ return
174
+
175
+ # Instead of position-based click, find the "..." element in DOM
176
+ click_result = await self.page.evaluate("""() => {
177
+ // Find the active/highlighted conversation item
178
+ const active = document.querySelector('[class*="active"], [class*="selected"]');
179
+ if (!active) return 'no-active';
180
+
181
+ // Walk down to find a clickable child that looks like "..."
182
+ // The "..." is often a button or div with no text (SVG only)
183
+ const walk = (node, depth) => {
184
+ if (depth > 10) return null;
185
+ for (const child of node.children || []) {
186
+ const tag = child.tagName;
187
+ const cls = (child.className || '').toString();
188
+ // Look for small icon-like elements
189
+ if ((tag === 'BUTTON' || tag === 'svg' || cls.includes('icon') || cls.includes('more') || cls.includes('menu') || cls.includes('action')) &&
190
+ child.offsetWidth < 40 && child.offsetWidth > 0) {
191
+ return child;
192
+ }
193
+ const found = walk(child, depth + 1);
194
+ if (found) return found;
195
+ }
196
+ return null;
197
+ };
198
+
199
+ const icon = walk(active, 0);
200
+ if (icon) {
201
+ icon.click();
202
+ return 'clicked:' + icon.tagName + ':' + (icon.className || '').substring(0, 40);
203
+ }
204
+
205
+ // Fallback: find any button/svg in active item
206
+ const btn = active.querySelector('button, [role="button"]');
207
+ if (btn) {
208
+ btn.click();
209
+ return 'fallback:' + btn.tagName;
210
+ }
211
+ return 'no-icon';
212
+ }""")
213
+ print(f"[delete_chat] icon click: {click_result}")
214
+ await asyncio.sleep(0.5)
215
 
216
+ # Search for "删除" or "Delete" anywhere on page
217
+ delete_btn = self.page.locator(
218
+ ':has-text("删除"), :has-text("Delete")'
219
+ ).first
220
+ delete_count = await delete_btn.count()
221
+
222
+ if delete_count == 0:
223
+ print(f"[delete_chat] no delete option found")
224
+ return
225
+
226
+ await delete_btn.click()
227
+ await asyncio.sleep(0.5)
228
+
229
+ # Confirm
230
+ confirm_btn = self.page.locator(
231
+ 'button:has-text("确认"), button:has-text("删除"), '
232
+ 'button:has-text("Confirm"), button:has-text("Delete")'
233
+ ).last
234
+ if await confirm_btn.count() > 0:
235
+ await confirm_btn.click()
236
+ await asyncio.sleep(1)
237
+ print(f"[delete_chat] done!")
238
+ else:
239
+ print(f"[delete_chat] no confirm btn")
240
+
241
+ except Exception as e:
242
+ print(f"[delete_chat] error: {e}")
243
 
244
  async def switch_model(self, model: str):
245
  try:
246
+ if 'reasoner' in model or 'thinking' in model or 'pro' in model:
247
  thinking_btn = self.page.locator('button:has-text("深度思考"), div:has-text("深度思考")').first
248
  if await thinking_btn.count() > 0:
249
  await thinking_btn.click()
 
291
  last_text = ""
292
  stable_count = 0
293
 
294
+ skip_phrases = ['深度思考', '智能搜索', '快速模式', '专家模式', '内容由 AI 生成', '开启新对话', '暂无历史对话', '今天', 'huan********dja@gmail.com']
295
 
296
  while time.time() < deadline:
297
  try:
298
+ text = await self.page.inner_text('body')
299
+
300
+ lines = text.split('\n')
301
+ response_started = False
302
+ response_text = []
 
 
 
 
 
 
 
 
 
303
 
 
 
304
  for line in lines:
305
  line = line.strip()
306
  if not line:
307
  continue
308
+
309
+ if line == '内容由 AI 生成,请仔细甄别':
310
+ break
311
+
312
  if any(phrase in line for phrase in skip_phrases):
313
  continue
 
314
 
315
+ if response_started:
316
+ response_text.append(line)
317
+
318
+ if prompt and prompt in line:
319
+ response_started = True
320
+
321
+ if response_text:
322
+ current_text = '\n'.join(response_text)
323
 
324
  if current_text != last_text:
325
  last_text = current_text
 
361
  last_text = ""
362
  stable_count = 0
363
 
364
+ skip_phrases = ['深度思考', '智能搜索', '快速模式', '专家模式', '内容由 AI 生成', '开启新对话', '暂无历史对话', '今天', 'huan********dja@gmail.com']
365
 
366
  await asyncio.sleep(3)
367
 
368
  while time.time() < deadline:
369
  try:
370
+ text = await self.page.inner_text('body')
371
+
372
+ lines = text.split('\n')
373
+ response_started = False
374
+ response_text = []
375
+
 
 
 
 
 
 
 
 
 
 
 
376
  for line in lines:
377
  line = line.strip()
378
  if not line:
379
  continue
380
+
381
+ if line == '内容由 AI 生成,请仔细甄别':
382
+ break
383
+
384
  if any(phrase in line for phrase in skip_phrases):
385
  continue
 
386
 
387
+ if response_started:
388
+ response_text.append(line)
389
+
390
+ if prompt and prompt in line:
391
+ response_started = True
392
+
393
+ if response_text:
394
+ current_text = '\n'.join(response_text)
395
 
396
  if current_text != last_text:
397
  new_chunk = current_text[len(last_text):]
main.py CHANGED
@@ -1,9 +1,16 @@
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
@@ -23,7 +30,7 @@ app.add_middleware(
23
  )
24
 
25
  config: Config = load_config()
26
- manager = AccountManager(max_concurrent_per_account=config.browser.max_concurrent_per_account)
27
 
28
 
29
  class Message(BaseModel):
@@ -56,18 +63,8 @@ async def list_models(authorization: str = Header(...)):
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
  }
@@ -78,10 +75,8 @@ 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:
@@ -102,16 +97,18 @@ async def chat_completions(
102
 
103
  prompt = request.messages[-1].content
104
 
 
 
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,7 +149,7 @@ async def chat_completions(
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
 
@@ -180,212 +177,6 @@ async def chat_completions(
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
- 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,
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 api.send_message(prompt, model=model, stream=False, timeout=120)
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
- 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
-
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
- 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
- {
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():
@@ -413,7 +204,6 @@ async def import_accounts(request: Request, admin_key: str = Header(...)):
413
 
414
  body = await request.json()
415
  accounts = body.get("accounts", [])
416
- default_proxy = body.get("default_proxy") or config.default_proxy
417
 
418
  if not accounts:
419
  raise HTTPException(status_code=400, detail="No accounts provided")
@@ -423,7 +213,7 @@ async def import_accounts(request: Request, admin_key: str = Header(...)):
423
  email = acc.get("email")
424
  password = acc.get("password")
425
  name = acc.get("name", "")
426
- proxy = acc.get("proxy") or default_proxy
427
 
428
  if email and password:
429
  manager.add_account(email, password, name, proxy)
@@ -442,56 +232,335 @@ async def list_accounts(admin_key: str = Header(...)):
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")
481
  async def startup():
482
  for acc in config.accounts:
483
- proxy = acc.proxy or config.default_proxy
484
  manager.add_account(
485
  email=acc.email,
486
  password=acc.password,
487
  name=acc.name,
488
- proxy=proxy,
489
  )
490
 
491
  print(f"Loaded {len(config.accounts)} accounts")
492
- print(f"Max concurrent per account: {config.browser.max_concurrent_per_account}")
493
- if config.default_proxy:
494
- print(f"Default proxy: {config.default_proxy}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
 
496
 
497
  def main():
 
1
  import asyncio
2
  import json
3
+ import os
4
  import time
5
  import uuid
6
+ from pathlib import Path
7
  from typing import Optional
8
 
9
+ from dotenv import load_dotenv
10
+
11
+ # 自动加载项目目录下的 .env
12
+ load_dotenv(Path(__file__).parent / ".env")
13
+
14
  from fastapi import FastAPI, HTTPException, Header, Request
15
  from fastapi.middleware.cors import CORSMiddleware
16
  from fastapi.responses import StreamingResponse
 
30
  )
31
 
32
  config: Config = load_config()
33
+ manager = AccountManager(max_inflight=2)
34
 
35
 
36
  class Message(BaseModel):
 
63
 
64
  return {
65
  "data": [
66
+ {"id": "deepseek-flash", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
67
+ {"id": "deepseek-pro", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
 
 
 
 
 
 
 
 
 
 
68
  ],
69
  "object": "list",
70
  }
 
75
  verify_api_key(authorization)
76
 
77
  models = {
78
+ "deepseek-flash": {"id": "deepseek-flash", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
79
+ "deepseek-pro": {"id": "deepseek-pro", "object": "model", "created": int(time.time()), "owned_by": "deepseek"},
 
 
80
  }
81
 
82
  if model_id in models:
 
97
 
98
  prompt = request.messages[-1].content
99
 
100
+ model = request.model
101
+
102
  account = await manager.acquire()
103
 
104
  try:
105
+ browser = await manager.get_or_create_browser_with_retry(account, headless=config.browser.headless)
106
 
107
  if request.stream:
108
  async def stream_with_cleanup():
109
  chunk_id = f"chatcmpl-{uuid.uuid4().hex[:8]}"
110
  try:
111
+ async for chunk in browser.stream_message(prompt, timeout=120, model=model):
112
  data = {
113
  "id": chunk_id,
114
  "object": "chat.completion.chunk",
 
149
  media_type="text/event-stream",
150
  )
151
 
152
+ response_text = await browser.send_message(prompt, timeout=120, model=model)
153
 
154
  await manager.release(account)
155
 
 
177
  raise HTTPException(status_code=503, detail=str(e))
178
 
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
  @app.get("/healthz")
182
  async def healthz():
 
204
 
205
  body = await request.json()
206
  accounts = body.get("accounts", [])
 
207
 
208
  if not accounts:
209
  raise HTTPException(status_code=400, detail="No accounts provided")
 
213
  email = acc.get("email")
214
  password = acc.get("password")
215
  name = acc.get("name", "")
216
+ proxy = acc.get("proxy")
217
 
218
  if email and password:
219
  manager.add_account(email, password, name, proxy)
 
232
  accounts.append({
233
  "email": email,
234
  "name": acc.name,
235
+ "in_use": acc.in_use,
 
 
236
  "logged_in": acc.logged_in,
237
  "error_count": acc.error_count,
238
  })
239
 
240
+ return {"accounts": accounts, "total": len(accounts)}
 
 
 
 
 
241
 
242
 
243
+ @app.get("/")
244
+ async def admin_panel():
245
+ from fastapi.responses import HTMLResponse
246
+ return HTMLResponse(content=ADMIN_HTML)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
 
249
  @app.on_event("startup")
250
  async def startup():
251
  for acc in config.accounts:
 
252
  manager.add_account(
253
  email=acc.email,
254
  password=acc.password,
255
  name=acc.name,
256
+ proxy=acc.proxy,
257
  )
258
 
259
  print(f"Loaded {len(config.accounts)} accounts")
260
+
261
+
262
+ ADMIN_HTML = """<!DOCTYPE html>
263
+ <html lang="zh-CN">
264
+ <head>
265
+ <meta charset="UTF-8">
266
+ <meta name="viewport" content="width=device-width,initial-scale=1">
267
+ <title>DS2API · 控制台</title>
268
+ <style>
269
+ :root{--bg:#060b10;--panel:#0b1219;--border:#15202e;--text:#9bb5cf;--dim:#3d5268;--accent:#5cc8ff;--green:#3fb950;--red:#f85149;--amber:#d29922;--row-hover:rgba(92,200,255,.03)}
270
+ *{box-sizing:border-box;margin:0;padding:0}
271
+ body{font-family:'JetBrains Mono','Sarasa Mono SC','Cascadia Code',Consolas,monospace;background:var(--bg);color:var(--text);font-size:12.5px;line-height:1.55;-webkit-font-smoothing:antialiased;min-height:100vh}
272
+ body::after{content:'';position:fixed;inset:0;background:radial-gradient(ellipse 60% 40% at 50% -10%,rgba(92,200,255,.025),transparent);pointer-events:none;z-index:0}
273
+
274
+ /* ─�� topbar ── */
275
+ .topbar{position:sticky;top:0;z-index:20;background:var(--panel);border-bottom:1px solid var(--border);padding:10px 20px;display:flex;align-items:center;gap:10px}
276
+ .topbar .logo{font-weight:800;font-size:13px;color:var(--accent);letter-spacing:1.5px}
277
+ .topbar .sep{color:var(--dim);margin:0 4px}
278
+ .topbar .mode{font-size:10px;color:var(--dim);border:1px solid var(--border);padding:2px 8px;letter-spacing:1px}
279
+ .topbar .stat-inline{display:flex;gap:16px;margin-left:auto;font-size:10px}
280
+ .topbar .stat-inline span{color:var(--dim)}
281
+ .topbar .stat-inline b{color:var(--accent);font-weight:800}
282
+ @media(max-width:700px){.topbar .stat-inline{display:none}}
283
+
284
+ /* ── main grid ── */
285
+ .main{position:relative;z-index:1;max-width:1100px;margin:0 auto;padding:20px 16px;display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start}
286
+ @media(max-width:800px){.main{grid-template-columns:1fr;padding:14px 10px;gap:12px}}
287
+
288
+ /* ── panel ── */
289
+ .panel{border:1px solid var(--border);background:var(--panel)}
290
+ .panel-head{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:rgba(92,200,255,.015)}
291
+ .panel-head h2{font-size:11px;color:var(--accent);letter-spacing:1.5px;font-weight:800}
292
+ .panel-head .hint{color:var(--dim);font-size:10px}
293
+ .panel-body{padding:14px}
294
+
295
+ /* ── form elements ── */
296
+ select,textarea,input[type=text]{width:100%;background:var(--bg);border:1px solid var(--border);padding:8px 10px;color:var(--text);font-family:inherit;font-size:12px;line-height:1.5}
297
+ select{padding:7px 10px;cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%233d5268'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}
298
+ select:focus,textarea:focus,input:focus{outline:none;border-color:var(--accent)}
299
+ textarea{min-height:80px;resize:vertical}
300
+ textarea::placeholder{color:var(--dim)}
301
+ .row{display:flex;gap:10px;align-items:center;margin-bottom:10px;flex-wrap:wrap}
302
+
303
+ /* ── buttons ── */
304
+ .btn{display:inline-flex;align-items:center;gap:5px;padding:7px 14px;border:1px solid var(--border);background:transparent;color:var(--text);cursor:pointer;font-family:inherit;font-size:11px;font-weight:700;letter-spacing:.8px;white-space:nowrap;transition:all .12s}
305
+ .btn:hover{border-color:var(--accent);color:var(--accent)}
306
+ .btn-accent{background:var(--accent);color:var(--bg);border-color:var(--accent)}
307
+ .btn-accent:hover{background:transparent;color:var(--accent)}
308
+ .btn-sm{padding:5px 10px;font-size:10px}
309
+
310
+ /* ── table ── */
311
+ .tbl{width:100%;border-collapse:collapse;font-size:11px}
312
+ .tbl thead{border-bottom:2px solid var(--border)}
313
+ .tbl th{padding:7px 6px;text-align:left;color:var(--dim);font-weight:700;font-size:9.5px;letter-spacing:.8px;white-space:nowrap}
314
+ .tbl td{padding:7px 6px;border-bottom:1px solid rgba(21,32,46,.6);word-break:break-all}
315
+ .tbl tr:hover td{background:var(--row-hover)}
316
+ @media(max-width:500px){.tbl th,.tbl td{font-size:10px;padding:6px 4px}}
317
+
318
+ /* ── badge ── */
319
+ .badge{display:inline-flex;align-items:center;gap:3px;padding:1px 7px;font-size:9.5px;font-weight:700;letter-spacing:.4px;white-space:nowrap}
320
+ .badge::before{content:'';width:4px;height:4px}
321
+ .badge-on{color:var(--green);border:1px solid rgba(63,185,80,.35)}.badge-on::before{background:var(--green)}
322
+ .badge-off{color:var(--red);border:1px solid rgba(248,81,73,.3)}.badge-off::before{background:var(--red)}
323
+ .badge-idle{color:var(--dim);border:1px solid var(--border)}.badge-idle::before{background:var(--dim)}
324
+
325
+ /* ── response area ── */
326
+ #response{background:var(--bg);border:1px solid var(--border);border-top:none;padding:12px;min-height:120px;max-height:400px;overflow-y:auto;font-size:12px;line-height:1.6;white-space:pre-wrap}
327
+ #response:empty::after{content:'等待发送…';color:var(--dim)}
328
+ .response-status{display:flex;justify-content:space-between;padding:6px 10px;font-size:10px;border-bottom:1px solid var(--border)}
329
+ .response-status .ok{color:var(--green)}.response-status .err{color:var(--red)}
330
+
331
+ /* ── toast ── */
332
+ .toast{position:fixed;top:20px;right:20px;z-index:99;padding:10px 18px;font-size:11px;font-weight:700;letter-spacing:.5px;animation:slide .25s;border:1px solid}
333
+ .toast-ok{background:rgba(63,185,80,.1);color:var(--green);border-color:rgba(63,185,80,.25)}
334
+ .toast-err{background:rgba(248,81,73,.1);color:var(--red);border-color:rgba(248,81,73,.25)}
335
+ @keyframes slide{from{transform:translateY(-8px);opacity:0}to{transform:translateY(0);opacity:1}}
336
+
337
+ /* ── misc ── */
338
+ .empty{color:var(--dim);padding:20px 0;text-align:center;font-size:11px}
339
+ .hidden{display:none}
340
+ .spacer{flex:1}
341
+ .ellipsis{max-width:160px;overflow:hidden;text-overflow:ellipsis;display:block}
342
+ .help{font-size:10px;color:var(--dim);margin-bottom:8px;opacity:.7}
343
+ .imp{padding:12px;margin-top:12px}
344
+ @media(max-width:500px){.hide-mobile{display:none}}
345
+ </style>
346
+ </head>
347
+ <body>
348
+ <div class="topbar">
349
+ <span class="logo">▸ DS2API</span>
350
+ <span class="mode">浏览器模式</span>
351
+ <div class="stat-inline" id="topStats">
352
+ <span>账号 <b>—</b></span>
353
+ <span>活跃 <b>—</b></span>
354
+ <span>可用 <b>—</b></span>
355
+ <span>在线 <b>—</b></span>
356
+ <span>排队 <b>—</b></span>
357
+ </div>
358
+ </div>
359
+
360
+ <div class="main">
361
+
362
+ <!-- ═══ 左栏:接口测试 ═══ -->
363
+ <div class="panel" style="grid-row:span 1">
364
+ <div class="panel-head">
365
+ <h2>接口测试</h2>
366
+ <span class="hint">/v1/chat/completions</span>
367
+ </div>
368
+ <div class="panel-body">
369
+ <div class="row">
370
+ <select id="model" style="flex:1">
371
+ <option value="deepseek-flash">deepseek-flash</option>
372
+ <option value="deepseek-pro">deepseek-pro</option>
373
+ </select>
374
+ <label style="font-size:11px;color:var(--dim);display:flex;align-items:center;gap:4px;white-space:nowrap">
375
+ <input type="checkbox" id="stream" checked> 流式
376
+ </label>
377
+ </div>
378
+ <textarea id="prompt" placeholder="输入消息…">你好,用一句话介绍你自己</textarea>
379
+ <div class="row" style="margin-top:10px;margin-bottom:0">
380
+ <button class="btn btn-accent" onclick="sendMsg()" id="sendBtn">▸ 发送</button>
381
+ <button class="btn btn-sm" onclick="sendMsg()" id="sendBtn2" style="display:none">▸ 发送</button>
382
+ <span id="reqStatus" style="font-size:10px;color:var(--dim)"></span>
383
+ <div class="spacer"></div>
384
+ <button class="btn btn-sm" onclick="document.getElementById('response').textContent=''">清空</button>
385
+ </div>
386
+ <div style="margin-top:12px;border:1px solid var(--border);border-bottom:none">
387
+ <div class="response-status">
388
+ <span id="respLabel">响应</span>
389
+ <span id="respTime"></span>
390
+ </div>
391
+ <div id="response"></div>
392
+ </div>
393
+ </div>
394
+ </div>
395
+
396
+ <!-- ═══ 右栏:账号管理 ═══ -->
397
+ <div class="panel">
398
+ <div class="panel-head">
399
+ <h2>账号管理</h2>
400
+ <button class="btn btn-sm" onclick="loadAccounts()">刷新</button>
401
+ </div>
402
+ <div class="panel-body" style="padding-bottom:8px">
403
+ <table class="tbl">
404
+ <thead><tr><th>邮箱</th><th class="hide-mobile">备注</th><th>登录</th><th>状态</th><th>禁言</th><th class="hide-mobile">错误</th></tr></thead>
405
+ <tbody id="tbl"><tr><td colspan="6" class="empty">加载中…</td></tr></tbody>
406
+ </table>
407
+ </div>
408
+ </div>
409
+
410
+ <div class="panel">
411
+ <div class="panel-head">
412
+ <h2>导入账号</h2>
413
+ </div>
414
+ <div class="panel-body">
415
+ <div class="help">格式:邮箱:密码 ,每行一个</div>
416
+ <textarea id="inp" placeholder="user@gmail.com:password&#10;user2@gmail.com:password" style="min-height:70px"></textarea>
417
+ <div class="row" style="margin-top:10px;margin-bottom:0">
418
+ <button class="btn btn-accent" onclick="doImport()">▸ 导入</button>
419
+ <span id="msg" style="font-size:10px;color:var(--dim)"></span>
420
+ </div>
421
+ </div>
422
+ </div>
423
+
424
+ </div>
425
+
426
+ <script>
427
+ const H=location.origin
428
+ const KEY='sbgptwcnmsbopenaiwdnmdcnmsbchat'
429
+
430
+ function toast(m,ok){
431
+ const e=document.createElement('div')
432
+ e.className='toast toast-'+(ok?'ok':'err')
433
+ e.textContent=m
434
+ document.body.appendChild(e)
435
+ setTimeout(()=>e.remove(),2500)
436
+ }
437
+
438
+ async function api(p,o={}){
439
+ const hd={};if(o.json)hd['Content-Type']='application/json'
440
+ Object.assign(hd,o.headers||{})
441
+ const r=await fetch(H+p,{headers:hd,method:o.method||'GET',body:o.body})
442
+ if(!r.ok){const t=await r.text();throw new Error(t||r.status)}
443
+ return r.json()
444
+ }
445
+
446
+ /* ── 接口测试 ── */
447
+ async function sendMsg(){
448
+ const model=document.getElementById('model').value
449
+ const prompt=document.getElementById('prompt').value.trim()
450
+ const stream=document.getElementById('stream').checked
451
+ const resp=document.getElementById('response')
452
+ const status=document.getElementById('reqStatus')
453
+ const timeEl=document.getElementById('respTime')
454
+ const btn=document.getElementById('sendBtn')
455
+
456
+ if(!prompt)return toast('请输入消息',0)
457
+ btn.disabled=true;btn.textContent='…'
458
+ resp.textContent='';timeEl.textContent=''
459
+
460
+ const t0=Date.now()
461
+ try{
462
+ const r=await fetch(H+'/v1/chat/completions',{
463
+ method:'POST',
464
+ headers:{'Content-Type':'application/json','Authorization':'Bearer '+KEY},
465
+ body:JSON.stringify({model,messages:[{role:'user',content:prompt}],stream})
466
+ })
467
+
468
+ if(stream){
469
+ const reader=r.body.getReader(),dec=new TextDecoder()
470
+ let full=''
471
+ while(1){
472
+ const{done,value}=await reader.read()
473
+ if(done)break
474
+ for(const line of dec.decode(value,{stream:true}).split('\\n')){
475
+ if(!line.startsWith('data: '))continue
476
+ const d=line.slice(6).trim()
477
+ if(d==='[DONE]')continue
478
+ try{const j=JSON.parse(d);const c=j.choices?.[0]?.delta?.content;if(c){full+=c;resp.textContent=full}}
479
+ catch(e){}
480
+ }
481
+ }
482
+ timeEl.textContent=((Date.now()-t0)/1000).toFixed(1)+'s'
483
+ status.textContent='流式完成';status.className='ok'
484
+ }else{
485
+ const d=await r.json()
486
+ resp.textContent=d.choices?.[0]?.message?.content||JSON.stringify(d,null,2)
487
+ timeEl.textContent=((Date.now()-t0)/1000).toFixed(1)+'s'
488
+ status.textContent=r.status+' OK';status.className='ok'
489
+ }
490
+ }catch(e){
491
+ resp.textContent='错误: '+e.message
492
+ status.textContent='失败';status.className='err'
493
+ }
494
+ btn.disabled=false;btn.textContent='▸ 发送'
495
+ }
496
+
497
+ /* ── 统计 & 账号 ── */
498
+ async function loadStats(){
499
+ try{
500
+ const s=await api('/readyz')
501
+ document.getElementById('topStats').innerHTML=
502
+ `<span>账号 <b>${s.accounts.total}</b></span>
503
+ <span>活跃 <b>${s.accounts.in_use}</b></span>
504
+ <span>可用 <b>${s.accounts.available}</b></span>
505
+ <span>在线 <b>${s.accounts.logged_in}</b></span>
506
+ <span>禁言 <b style="color:var(--red)">${s.accounts.muted||0}</b></span>
507
+ <span>排队 <b>${s.accounts.queue_size}</b></span>`
508
+ }catch(e){}
509
+ }
510
+ async function loadAccounts(){
511
+ try{
512
+ const d=await api('/admin/accounts',{headers:{'admin-key':'admin'}})
513
+ let r=''
514
+ for(const a of d.accounts){
515
+ r+=`<tr>
516
+ <td><span class="ellipsis">${a.email}</span></td>
517
+ <td class="hide-mobile">${a.name||'—'}</td>
518
+ <td><span class="badge ${a.logged_in?'badge-on':'badge-off'}">${a.logged_in?'在线':'离线'}</span></td>
519
+ <td><span class="badge ${a.in_use?'badge-on':'badge-idle'}">${a.in_use?'使用中':'空闲'}</span></td>
520
+ <td>${a.is_muted?`<span class="badge badge-off" title="${a.muted_until||'已禁言'}">禁言</span>`:'<span class="badge badge-idle">正常</span>'}</td>
521
+ <td class="hide-mobile">${a.error_count>0?'<span class="badge badge-off">'+a.error_count+'</span>':'—'}</td>
522
+ </tr>`
523
+ }
524
+ document.getElementById('tbl').innerHTML=r||'<tr><td colspan="6" class="empty">暂无账号</td></tr>'
525
+ }catch(e){
526
+ document.getElementById('tbl').innerHTML='<tr><td colspan="6" style="color:var(--red)">'+e.message+'</td></tr>'
527
+ }
528
+ }
529
+ async function loadAll(){await loadStats();await loadAccounts()}
530
+
531
+ async function doImport(){
532
+ const v=document.getElementById('inp').value.trim()
533
+ if(!v)return toast('请输入账号',0)
534
+ const accts=[]
535
+ for(const l of v.split('\\n')){
536
+ const t=l.trim();if(!t)continue
537
+ const p=t.split(':',3)
538
+ if(p.length>=2)accts.push({email:p[0].trim(),password:p[1],name:p[2]||''})
539
+ }
540
+ if(!accts.length)return toast('格式错误',0)
541
+ try{
542
+ const d=await api('/admin/accounts/import',{
543
+ method:'POST',json:true,
544
+ body:JSON.stringify({accounts:accts}),
545
+ headers:{'admin-key':'admin'}
546
+ })
547
+ document.getElementById('inp').value=''
548
+ document.getElementById('msg').textContent='已导入 '+d.imported+' 个'
549
+ toast('成功导入 '+d.imported+' 个',1)
550
+ loadAll()
551
+ }catch(e){toast(e.message,0)}
552
+ }
553
+
554
+ // 回车发送
555
+ document.getElementById('prompt').addEventListener('keydown',e=>{
556
+ if(e.ctrlKey&&e.key==='Enter')sendMsg()
557
+ })
558
+
559
+ loadAll()
560
+ setInterval(loadAll,12000)
561
+ </script>
562
+ </body>
563
+ </html>"""
564
 
565
 
566
  def main():
test.html DELETED
@@ -1,306 +0,0 @@
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:socks5://127.0.0.1:1080"></textarea>
78
- </div>
79
- <div class="form-group">
80
- <label>默认代理 (可选,格式: socks5://127.0.0.1:1080)</label>
81
- <input type="text" id="defaultProxy" placeholder="socks5://127.0.0.1:1080">
82
- </div>
83
- <div style="display: flex; gap: 10px;">
84
- <button onclick="importAccounts()" style="flex: 1;">导入账号</button>
85
- <button onclick="loadAccounts()" style="flex: 1; background: #6c757d;">刷新账号列表</button>
86
- </div>
87
- <div id="accountsList" class="response" style="margin-top: 10px; max-height: 200px; overflow-y: auto;">点击"刷新账号列表"查看...</div>
88
- </div>
89
- </div>
90
-
91
- <script>
92
- async function sendMessage() {
93
- const apiUrl = document.getElementById('apiUrl').value;
94
- const apiKey = document.getElementById('apiKey').value;
95
- const model = document.getElementById('model').value;
96
- const message = document.getElementById('message').value;
97
- const isStream = document.getElementById('stream').checked;
98
- const responseDiv = document.getElementById('response');
99
- const statusSpan = document.getElementById('status');
100
- const timeSpan = document.getElementById('time');
101
- const sendBtn = document.getElementById('sendBtn');
102
-
103
- if (!message.trim()) {
104
- responseDiv.textContent = '请输入消息';
105
- responseDiv.className = 'response error';
106
- return;
107
- }
108
-
109
- sendBtn.disabled = true;
110
- sendBtn.textContent = '发送中...';
111
- responseDiv.textContent = '正在等待响应...';
112
- responseDiv.className = 'response loading';
113
- statusSpan.textContent = '';
114
- timeSpan.textContent = '';
115
-
116
- const startTime = Date.now();
117
-
118
- try {
119
- const response = await fetch(`${apiUrl}/v1/chat/completions`, {
120
- method: 'POST',
121
- headers: {
122
- 'Content-Type': 'application/json',
123
- 'Authorization': `Bearer ${apiKey}`
124
- },
125
- body: JSON.stringify({
126
- model: model,
127
- messages: [{ role: 'user', content: message }],
128
- stream: isStream
129
- })
130
- });
131
-
132
- if (isStream) {
133
- const reader = response.body.getReader();
134
- const decoder = new TextDecoder();
135
- let fullContent = '';
136
-
137
- while (true) {
138
- const { done, value } = await reader.read();
139
- if (done) break;
140
-
141
- const chunk = decoder.decode(value, { stream: true });
142
- const lines = chunk.split('\n');
143
-
144
- for (const line of lines) {
145
- if (line.startsWith('data: ')) {
146
- const data = line.slice(6).trim();
147
- if (data === '[DONE]') continue;
148
-
149
- try {
150
- const json = JSON.parse(data);
151
- const content = json.choices?.[0]?.delta?.content;
152
- if (content) {
153
- fullContent += content;
154
- responseDiv.textContent = fullContent;
155
- responseDiv.className = 'response success';
156
- }
157
- } catch (e) {
158
- // Skip invalid JSON
159
- }
160
- }
161
- }
162
- }
163
-
164
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
165
- statusSpan.textContent = `状态: 流式完成`;
166
- timeSpan.textContent = `耗时: ${elapsed}s`;
167
- } else {
168
- const data = await response.json();
169
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
170
-
171
- if (response.ok) {
172
- const content = data.choices?.[0]?.message?.content || '无响应内容';
173
- responseDiv.textContent = content;
174
- responseDiv.className = 'response success';
175
- statusSpan.textContent = `状态: ${response.status || 200} OK`;
176
- } else {
177
- responseDiv.textContent = `错误: ${data.detail || JSON.stringify(data)}`;
178
- responseDiv.className = 'response error';
179
- statusSpan.textContent = `状态: ${response.status || '错误'}`;
180
- }
181
-
182
- timeSpan.textContent = `耗时: ${elapsed}s`;
183
- }
184
-
185
- } catch (error) {
186
- responseDiv.textContent = `请求失败: ${error.message}`;
187
- responseDiv.className = 'response error';
188
- statusSpan.textContent = '错误';
189
- } finally {
190
- sendBtn.disabled = false;
191
- sendBtn.textContent = '发送消息';
192
- }
193
- }
194
-
195
- document.getElementById('message').addEventListener('keydown', function(e) {
196
- if (e.ctrlKey && e.key === 'Enter') {
197
- sendMessage();
198
- }
199
- });
200
-
201
- async function importAccounts() {
202
- const apiUrl = document.getElementById('apiUrl').value;
203
- const apiKey = document.getElementById('apiKey').value;
204
- const accountsText = document.getElementById('accountsInput').value.trim();
205
- const defaultProxy = document.getElementById('defaultProxy').value.trim();
206
- const accountsList = document.getElementById('accountsList');
207
-
208
- if (!accountsText) {
209
- accountsList.textContent = '请输入账号信息';
210
- accountsList.className = 'response error';
211
- return;
212
- }
213
-
214
- const lines = accountsText.split('\n').filter(line => line.trim());
215
- const accounts = [];
216
-
217
- for (const line of lines) {
218
- const parts = line.split(':');
219
- if (parts.length >= 2) {
220
- accounts.push({
221
- email: parts[0].trim(),
222
- password: parts[1].trim(),
223
- proxy: parts[2] ? parts[2].trim() : null
224
- });
225
- }
226
- }
227
-
228
- if (accounts.length === 0) {
229
- accountsList.textContent = '格式错误,请使用 email:password 或 email:password:proxy 格式';
230
- accountsList.className = 'response error';
231
- return;
232
- }
233
-
234
- accountsList.textContent = '导入中...';
235
- accountsList.className = 'response loading';
236
-
237
- try {
238
- const body = { accounts };
239
- if (defaultProxy) {
240
- body.default_proxy = defaultProxy;
241
- }
242
-
243
- const response = await fetch(`${apiUrl}/admin/accounts/import`, {
244
- method: 'POST',
245
- headers: {
246
- 'Content-Type': 'application/json',
247
- 'Authorization': `Bearer ${apiKey}`,
248
- 'admin-key': 'admin'
249
- },
250
- body: JSON.stringify(body)
251
- });
252
-
253
- const data = await response.json();
254
-
255
- if (response.ok) {
256
- accountsList.textContent = JSON.stringify(data, null, 2);
257
- accountsList.className = 'response success';
258
- document.getElementById('accountsInput').value = '';
259
- } else {
260
- accountsList.textContent = `错误: ${data.detail || JSON.stringify(data)}`;
261
- accountsList.className = 'response error';
262
- }
263
- } catch (error) {
264
- accountsList.textContent = `请求失败: ${error.message}`;
265
- accountsList.className = 'response error';
266
- }
267
- }
268
-
269
- async function loadAccounts() {
270
- const apiUrl = document.getElementById('apiUrl').value;
271
- const apiKey = document.getElementById('apiKey').value;
272
- const accountsList = document.getElementById('accountsList');
273
-
274
- accountsList.textContent = '加载中...';
275
- accountsList.className = 'response loading';
276
-
277
- try {
278
- const response = await fetch(`${apiUrl}/admin/accounts`, {
279
- method: 'GET',
280
- headers: {
281
- 'Authorization': `Bearer ${apiKey}`,
282
- 'admin-key': 'admin'
283
- }
284
- });
285
-
286
- const data = await response.json();
287
-
288
- if (response.ok) {
289
- let html = `账号数量: ${data.total}\n\n`;
290
- for (const acc of data.accounts) {
291
- html += `邮箱: ${acc.email}\n状态: ${acc.status}\n使用次数: ${acc.usage_count}\n\n`;
292
- }
293
- accountsList.textContent = html;
294
- accountsList.className = 'response success';
295
- } else {
296
- accountsList.textContent = `错误: ${data.detail || JSON.stringify(data)}`;
297
- accountsList.className = 'response error';
298
- }
299
- } catch (error) {
300
- accountsList.textContent = `请求失败: ${error.message}`;
301
- accountsList.className = 'response error';
302
- }
303
- }
304
- </script>
305
- </body>
306
- </html>