Spaces:
Running
Running
huanx520 commited on
Commit ·
2940fed
1
Parent(s): b98e00b
feat: 禁言检测 - 面板显示禁言状态与到期时间
Browse files- account_manager.py +20 -0
- deepseek_browser.py +127 -16
- 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 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 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="
|
| 523 |
}catch(e){
|
| 524 |
-
document.getElementById('tbl').innerHTML='<tr><td colspan="
|
| 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()}
|