huanx520 commited on
Commit
2940fed
·
1 Parent(s): b98e00b

feat: 禁言检测 - 面板显示禁言状态与到期时间

Browse files
Files changed (3) hide show
  1. account_manager.py +20 -0
  2. deepseek_browser.py +127 -16
  3. main.py +6 -4
account_manager.py CHANGED
@@ -16,6 +16,8 @@ class Account:
16
  in_use: bool = False
17
  error_count: int = 0
18
  logged_in: bool = False
 
 
19
 
20
 
21
  class AccountManager:
@@ -85,6 +87,9 @@ class AccountManager:
85
  )
86
  await account.browser.start()
87
  account.logged_in = True
 
 
 
88
  return account.browser
89
  except Exception as e:
90
  print(f"Error creating browser: {e}")
@@ -112,10 +117,25 @@ class AccountManager:
112
  in_use = sum(1 for a in self.accounts.values() if a.in_use)
113
  available = sum(1 for a in self.accounts.values() if not a.in_use and a.error_count < 3)
114
  logged_in = sum(1 for a in self.accounts.values() if a.logged_in)
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  return {
116
  "total": total,
117
  "in_use": in_use,
118
  "available": available,
119
  "logged_in": logged_in,
 
120
  "queue_size": len(self.queue),
 
121
  }
 
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:
 
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}")
 
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
  }
deepseek_browser.py CHANGED
@@ -61,15 +61,50 @@ class DeepSeekBrowser:
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
 
@@ -113,22 +148,98 @@ class DeepSeekBrowser:
113
 
114
  async def delete_chat(self):
115
  try:
116
- more_btn = self.page.locator('button:has-text("更多"), .ds-icon-button:has-text("...")').first
117
- if await more_btn.count() > 0:
118
- await more_btn.click()
119
- await asyncio.sleep(0.5)
120
-
121
- delete_btn = self.page.locator('button:has-text("删除"), div:has-text("删除对话")').first
122
- if await delete_btn.count() > 0:
123
- await delete_btn.click()
124
- await asyncio.sleep(0.5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- confirm_btn = self.page.locator('button:has-text("确认"), button:has-text("删除")').last
127
- if await confirm_btn.count() > 0:
128
- await confirm_btn.click()
129
- await asyncio.sleep(1)
130
- except Exception:
131
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  async def switch_model(self, model: str):
134
  try:
 
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
 
 
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:
main.py CHANGED
@@ -401,8 +401,8 @@ textarea::placeholder{color:var(--dim)}
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 class="hide-mobile">错误</th></tr></thead>
405
- <tbody id="tbl"><tr><td colspan="5" class="empty">加载中…</td></tr></tbody>
406
  </table>
407
  </div>
408
  </div>
@@ -503,6 +503,7 @@ async function loadStats(){
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>${s.accounts.queue_size}</b></span>`
507
  }catch(e){}
508
  }
@@ -516,12 +517,13 @@ async function loadAccounts(){
516
  <td class="hide-mobile">${a.name||'—'}</td>
517
  <td><span class="badge ${a.logged_in?'badge-on':'badge-off'}">${a.logged_in?'在线':'离线'}</span></td>
518
  <td><span class="badge ${a.in_use?'badge-on':'badge-idle'}">${a.in_use?'使用中':'空闲'}</span></td>
 
519
  <td class="hide-mobile">${a.error_count>0?'<span class="badge badge-off">'+a.error_count+'</span>':'—'}</td>
520
  </tr>`
521
  }
522
- document.getElementById('tbl').innerHTML=r||'<tr><td colspan="5" class="empty">暂无账号</td></tr>'
523
  }catch(e){
524
- document.getElementById('tbl').innerHTML='<tr><td colspan="5" style="color:var(--red)">'+e.message+'</td></tr>'
525
  }
526
  }
527
  async function loadAll(){await loadStats();await loadAccounts()}
 
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>
 
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
  }
 
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()}