nacho commited on
Commit
f7df793
·
1 Parent(s): d12c6be

feat: detailed error reporting — per-account error messages, screenshot context, UI tooltips

Browse files
Files changed (4) hide show
  1. account_manager.py +19 -33
  2. deepseek_browser.py +17 -7
  3. main.py +19 -13
  4. static/index.html +10 -8
account_manager.py CHANGED
@@ -1,5 +1,6 @@
1
  import asyncio
2
  import logging
 
3
  from collections import deque
4
  from dataclasses import dataclass, field
5
  from typing import Dict, Optional
@@ -9,8 +10,6 @@ from deepseek_browser import DeepSeekBrowser
9
  logger = logging.getLogger(__name__)
10
 
11
 
12
- import time
13
-
14
  @dataclass
15
  class Account:
16
  email: str
@@ -20,6 +19,7 @@ class Account:
20
  browser: Optional[DeepSeekBrowser] = field(default=None, repr=False)
21
  in_use: bool = False
22
  error_count: int = 0
 
23
  logged_in: bool = False
24
  is_muted: bool = False
25
  muted_until: str = ""
@@ -35,10 +35,7 @@ class AccountManager:
35
 
36
  def add_account(self, email: str, password: str, name: str = "", proxy: Optional[str] = None):
37
  self.accounts[email] = Account(
38
- email=email,
39
- password=password,
40
- name=name,
41
- proxy=proxy,
42
  )
43
 
44
  async def acquire(self) -> Account:
@@ -48,23 +45,19 @@ class AccountManager:
48
  account.in_use = True
49
  account.last_used = time.time()
50
  return account
51
-
52
  return await self._wait_for_account()
53
 
54
  async def _wait_for_account(self) -> Account:
55
  event = asyncio.Event()
56
  async with self._lock:
57
  self.queue.append(event)
58
-
59
  await event.wait()
60
-
61
  async with self._lock:
62
  for account in self.accounts.values():
63
  if not account.in_use and account.error_count < 3 and not account.is_muted:
64
  account.in_use = True
65
  account.last_used = time.time()
66
  return account
67
-
68
  raise RuntimeError("No account available")
69
 
70
  async def release(self, account: Account):
@@ -75,9 +68,10 @@ class AccountManager:
75
  event = self.queue.popleft()
76
  event.set()
77
 
78
- async def mark_error(self, account: Account):
79
  async with self._lock:
80
  account.error_count += 1
 
81
  account.in_use = False
82
  if self.queue:
83
  event = self.queue.popleft()
@@ -99,21 +93,20 @@ class AccountManager:
99
  if account.browser is None:
100
  await self._enforce_browser_limit()
101
  account.browser = DeepSeekBrowser(
102
- email=account.email,
103
- password=account.password,
104
- profile_dir="./profiles",
105
- headless=headless,
106
- humanize=True,
107
- proxy=account.proxy,
108
  )
109
  await account.browser.start()
110
  account.logged_in = True
111
- # Check mute status
 
112
  account.is_muted = account.browser.is_muted()
113
  account.muted_until = account.browser.muted_until()
114
  return account.browser
115
  except Exception as e:
116
  logger.error("Error creating browser for %s: %s", account.email, e)
 
117
  await self.close_browser(account)
118
  raise
119
 
@@ -141,22 +134,15 @@ class AccountManager:
141
  muted = sum(1 for a in self.accounts.values() if a.is_muted)
142
  accounts_list = [
143
  {
144
- "email": a.email,
145
- "name": a.name,
146
- "in_use": a.in_use,
147
- "logged_in": a.logged_in,
148
- "is_muted": a.is_muted,
149
- "muted_until": a.muted_until,
150
- "error_count": a.error_count,
151
  }
152
  for a in self.accounts.values()
153
  ]
154
  return {
155
- "total": total,
156
- "in_use": in_use,
157
- "available": available,
158
- "logged_in": logged_in,
159
- "muted": muted,
160
- "queue_size": len(self.queue),
161
- "accounts": accounts_list,
162
- }
 
1
  import asyncio
2
  import logging
3
+ import time
4
  from collections import deque
5
  from dataclasses import dataclass, field
6
  from typing import Dict, Optional
 
10
  logger = logging.getLogger(__name__)
11
 
12
 
 
 
13
  @dataclass
14
  class Account:
15
  email: str
 
19
  browser: Optional[DeepSeekBrowser] = field(default=None, repr=False)
20
  in_use: bool = False
21
  error_count: int = 0
22
+ last_error: str = ""
23
  logged_in: bool = False
24
  is_muted: bool = False
25
  muted_until: str = ""
 
35
 
36
  def add_account(self, email: str, password: str, name: str = "", proxy: Optional[str] = None):
37
  self.accounts[email] = Account(
38
+ email=email, password=password, name=name, proxy=proxy,
 
 
 
39
  )
40
 
41
  async def acquire(self) -> Account:
 
45
  account.in_use = True
46
  account.last_used = time.time()
47
  return account
 
48
  return await self._wait_for_account()
49
 
50
  async def _wait_for_account(self) -> Account:
51
  event = asyncio.Event()
52
  async with self._lock:
53
  self.queue.append(event)
 
54
  await event.wait()
 
55
  async with self._lock:
56
  for account in self.accounts.values():
57
  if not account.in_use and account.error_count < 3 and not account.is_muted:
58
  account.in_use = True
59
  account.last_used = time.time()
60
  return account
 
61
  raise RuntimeError("No account available")
62
 
63
  async def release(self, account: Account):
 
68
  event = self.queue.popleft()
69
  event.set()
70
 
71
+ async def mark_error(self, account: Account, error_msg: str = ""):
72
  async with self._lock:
73
  account.error_count += 1
74
+ account.last_error = error_msg
75
  account.in_use = False
76
  if self.queue:
77
  event = self.queue.popleft()
 
93
  if account.browser is None:
94
  await self._enforce_browser_limit()
95
  account.browser = DeepSeekBrowser(
96
+ email=account.email, password=account.password,
97
+ profile_dir="./profiles", headless=headless,
98
+ humanize=True, proxy=account.proxy,
 
 
 
99
  )
100
  await account.browser.start()
101
  account.logged_in = True
102
+ account.error_count = 0
103
+ account.last_error = ""
104
  account.is_muted = account.browser.is_muted()
105
  account.muted_until = account.browser.muted_until()
106
  return account.browser
107
  except Exception as e:
108
  logger.error("Error creating browser for %s: %s", account.email, e)
109
+ await self.mark_error(account, str(e))
110
  await self.close_browser(account)
111
  raise
112
 
 
134
  muted = sum(1 for a in self.accounts.values() if a.is_muted)
135
  accounts_list = [
136
  {
137
+ "email": a.email, "name": a.name,
138
+ "in_use": a.in_use, "logged_in": a.logged_in,
139
+ "is_muted": a.is_muted, "muted_until": a.muted_until,
140
+ "error_count": a.error_count, "last_error": a.last_error,
 
 
 
141
  }
142
  for a in self.accounts.values()
143
  ]
144
  return {
145
+ "total": total, "in_use": in_use, "available": available,
146
+ "logged_in": logged_in, "muted": muted,
147
+ "queue_size": len(self.queue), "accounts": accounts_list,
148
+ }
 
 
 
 
deepseek_browser.py CHANGED
@@ -32,11 +32,21 @@ class DeepSeekBrowser:
32
  def _safe_email(self):
33
  return self.email.replace("@", "_at_").replace("+", "_plus_")
34
 
35
- async def _save_screenshot(self, tag):
36
  try:
37
  SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
38
  path = SCREENSHOT_DIR / f"{tag}_{self._safe_email()}.png"
39
  await self.page.screenshot(path=str(path))
 
 
 
 
 
 
 
 
 
 
40
  logger.error("Screenshot saved to %s", path)
41
  except Exception as e:
42
  logger.debug("Screenshot failed: %s", e)
@@ -165,7 +175,7 @@ class DeepSeekBrowser:
165
  await email_input.fill(self.email)
166
  await asyncio.sleep(0.1)
167
  except Exception as e:
168
- await self._save_screenshot("login_fail_email")
169
  logger.error("Email input error: %s", e)
170
  raise
171
 
@@ -175,7 +185,7 @@ class DeepSeekBrowser:
175
  await pwd.fill(self.password)
176
  await asyncio.sleep(0.1)
177
  except Exception as e:
178
- await self._save_screenshot("login_fail_password")
179
  logger.error("Password input error: %s", e)
180
  raise
181
 
@@ -184,7 +194,7 @@ class DeepSeekBrowser:
184
  await btn.click()
185
  await asyncio.sleep(1.5)
186
  except Exception as e:
187
- await self._save_screenshot("login_fail_button")
188
  logger.error("Login button error: %s", e)
189
  raise
190
 
@@ -193,9 +203,9 @@ class DeepSeekBrowser:
193
  self._logged_in = True
194
  self._ready = True
195
  logger.info("Login successful for %s", self.email)
196
- except Exception:
197
- await self._save_screenshot("login_fail_final")
198
- raise Exception("Login failed — screenshot at /static/screenshots/")
199
 
200
  async def new_chat(self):
201
  try:
 
32
  def _safe_email(self):
33
  return self.email.replace("@", "_at_").replace("+", "_plus_")
34
 
35
+ async def _save_screenshot(self, tag, error_msg=""):
36
  try:
37
  SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
38
  path = SCREENSHOT_DIR / f"{tag}_{self._safe_email()}.png"
39
  await self.page.screenshot(path=str(path))
40
+ # Write companion error context file
41
+ if error_msg:
42
+ txt_path = SCREENSHOT_DIR / f"{tag}_{self._safe_email()}.txt"
43
+ txt_path.write_text(
44
+ f"Account: {self.email}\n"
45
+ f"Tag: {tag}\n"
46
+ f"Time: {time.strftime('%Y-%m-%d %H:%M:%S')}\n"
47
+ f"Error: {error_msg}\n",
48
+ encoding="utf-8"
49
+ )
50
  logger.error("Screenshot saved to %s", path)
51
  except Exception as e:
52
  logger.debug("Screenshot failed: %s", e)
 
175
  await email_input.fill(self.email)
176
  await asyncio.sleep(0.1)
177
  except Exception as e:
178
+ await self._save_screenshot("login_fail_email", str(e))
179
  logger.error("Email input error: %s", e)
180
  raise
181
 
 
185
  await pwd.fill(self.password)
186
  await asyncio.sleep(0.1)
187
  except Exception as e:
188
+ await self._save_screenshot("login_fail_password", str(e))
189
  logger.error("Password input error: %s", e)
190
  raise
191
 
 
194
  await btn.click()
195
  await asyncio.sleep(1.5)
196
  except Exception as e:
197
+ await self._save_screenshot("login_fail_button", str(e))
198
  logger.error("Login button error: %s", e)
199
  raise
200
 
 
203
  self._logged_in = True
204
  self._ready = True
205
  logger.info("Login successful for %s", self.email)
206
+ except Exception as e:
207
+ await self._save_screenshot("login_fail_final", str(e))
208
+ raise Exception("Login failed")
209
 
210
  async def new_chat(self):
211
  try:
main.py CHANGED
@@ -308,7 +308,7 @@ async def chat_completions(
308
  }
309
 
310
  except Exception as e:
311
- await manager.mark_error(account)
312
  logger.error("Chat completion error for model=%s: %s", request.model, e)
313
  raise HTTPException(status_code=503, detail=str(e))
314
 
@@ -550,7 +550,7 @@ async def admin_chat(request: Request, admin_key: str = Header(...)):
550
  "usage": {"prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens, "total_tokens": prompt_tokens + completion_tokens},
551
  }
552
  except Exception as e:
553
- await manager.mark_error(account)
554
  logger.error("Admin chat error: %s", e)
555
  raise HTTPException(status_code=503, detail=str(e))
556
 
@@ -666,17 +666,23 @@ async def list_screenshots(admin_key: str = Header(...)):
666
  if not SCREENSHOT_DIR.exists():
667
  return {"screenshots": []}
668
  files = sorted(SCREENSHOT_DIR.glob("*.png"), key=lambda p: p.stat().st_mtime, reverse=True)
669
- return {
670
- "screenshots": [
671
- {
672
- "name": f.name,
673
- "url": f"/static/screenshots/{f.name}",
674
- "size_kb": round(f.stat().st_size / 1024, 1),
675
- "time": time.strftime("%m-%d %H:%M", time.localtime(f.stat().st_mtime)),
676
- }
677
- for f in files[:50]
678
- ]
679
- }
 
 
 
 
 
 
680
 
681
 
682
  @app.post("/admin/logs/level")
 
308
  }
309
 
310
  except Exception as e:
311
+ await manager.mark_error(account, str(e))
312
  logger.error("Chat completion error for model=%s: %s", request.model, e)
313
  raise HTTPException(status_code=503, detail=str(e))
314
 
 
550
  "usage": {"prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens, "total_tokens": prompt_tokens + completion_tokens},
551
  }
552
  except Exception as e:
553
+ await manager.mark_error(account, str(e))
554
  logger.error("Admin chat error: %s", e)
555
  raise HTTPException(status_code=503, detail=str(e))
556
 
 
666
  if not SCREENSHOT_DIR.exists():
667
  return {"screenshots": []}
668
  files = sorted(SCREENSHOT_DIR.glob("*.png"), key=lambda p: p.stat().st_mtime, reverse=True)
669
+ results = []
670
+ for f in files[:50]:
671
+ txt_path = f.with_suffix(".txt")
672
+ error_text = ""
673
+ if txt_path.exists():
674
+ try:
675
+ error_text = txt_path.read_text(encoding="utf-8").strip()
676
+ except Exception:
677
+ pass
678
+ results.append({
679
+ "name": f.name,
680
+ "url": f"/static/screenshots/{f.name}",
681
+ "size_kb": round(f.stat().st_size / 1024, 1),
682
+ "time": time.strftime("%m-%d %H:%M", time.localtime(f.stat().st_mtime)),
683
+ "error": error_text,
684
+ })
685
+ return {"screenshots": results}
686
 
687
 
688
  @app.post("/admin/logs/level")
static/index.html CHANGED
@@ -591,7 +591,7 @@ async function loadAccounts(){
591
  <td><span class="badge ${a.logged_in?'badge-on':'badge-off'}">${a.logged_in?'在线':'离线'}</span></td>
592
  <td><span class="badge ${a.in_use?'badge-on':'badge-idle'}">${a.in_use?'使用中':'空闲'}</span></td>
593
  <td class="hide-xs">${a.is_muted?`<span class="badge badge-warn" title="${a.muted_until||''}">禁言</span>`:'<span class="badge badge-idle">正常</span>'}</td>
594
- <td class="hide-xs">${a.error_count>0?`<span class="badge badge-off">${a.error_count}</span>`:'—'}</td>
595
  <td><button class="btn btn-sm" onclick="doLoginAccount('${a.email}')">${a.logged_in?'重连':'登录'}</button></td>
596
  </tr>`;
597
  }
@@ -716,13 +716,15 @@ async function loadScreenshots(){
716
  try{
717
  const d=await api('/admin/screenshots',{headers:{'admin-key':getAdminKey()}});
718
  const el=document.getElementById('ssList');
719
- if(!d.screenshots||!d.screenshots.length){el.innerHTML='<span>暂无截图</span>';return}
720
- el.innerHTML=d.screenshots.map(s=>
721
- `<a href="${s.url}" target="_blank" style="display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:var(--surface-solid);border:1px solid var(--border);border-radius:6px;color:var(--text);text-decoration:none;font-size:11px" title="${s.name}&#10;${s.time} · ${s.size_kb}KB">
722
- 🖼️ ${s.name.replace('login_fail_','').replace('_at_gmail.com','').substring(0,25)}
723
- <span style="color:var(--text-dim);font-size:10px">${s.time}</span>
724
- </a>`
725
- ).join('');
 
 
726
  }catch(e){}
727
  }
728
  </script>
 
591
  <td><span class="badge ${a.logged_in?'badge-on':'badge-off'}">${a.logged_in?'在线':'离线'}</span></td>
592
  <td><span class="badge ${a.in_use?'badge-on':'badge-idle'}">${a.in_use?'使用中':'空闲'}</span></td>
593
  <td class="hide-xs">${a.is_muted?`<span class="badge badge-warn" title="${a.muted_until||''}">禁言</span>`:'<span class="badge badge-idle">正常</span>'}</td>
594
+ <td class="hide-xs">${a.error_count>0?`<span class="badge badge-off" title="${(a.last_error||'').replace(/"/g,'&quot;').replace(/'/g,"&#39;")}">${a.error_count}</span>`:'—'}</td>
595
  <td><button class="btn btn-sm" onclick="doLoginAccount('${a.email}')">${a.logged_in?'重连':'登录'}</button></td>
596
  </tr>`;
597
  }
 
716
  try{
717
  const d=await api('/admin/screenshots',{headers:{'admin-key':getAdminKey()}});
718
  const el=document.getElementById('ssList');
719
+ if(!d.screenshots||!d.screenshots.length){el.innerHTML='<span style="color:var(--text-muted)">暂无截图</span>';return}
720
+ el.innerHTML=d.screenshots.map(s=>{
721
+ const label=s.name.replace('login_fail_','').replace('_at_gmail.com','');
722
+ const errPart=s.error?`<div style="font-size:10px;color:var(--red);margin-top:3px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${s.error.replace(/</g,'&lt;').split('\\n')[0].substring(0,80)}</div>`:'';
723
+ return `<a href="${s.url}" target="_blank" style="display:inline-block;padding:6px 10px;background:var(--surface-solid);border:1px solid var(--border);border-radius:6px;color:var(--text);text-decoration:none;font-size:11px;margin:3px" title="${s.error||s.name}">
724
+ 🖼️ ${label.substring(0,20)} <span style="color:var(--text-dim);font-size:10px">${s.time} · ${s.size_kb}KB</span>
725
+ ${errPart}
726
+ </a>`;
727
+ }).join('');
728
  }catch(e){}
729
  }
730
  </script>