feat: 风险分定时衰减 + 密码提示修复 + 浏览器池API + next回跳
1. 风险分衰减定时任务: - services/scheduler.py: 每天 CST 04:00 自动执行 decay_scores() - 支持 RISK_SCORE_DECAY_TIME_CST 环境变量覆盖 2. 密码长度提示统一为8位: - app-frontend/src/pages/RegisterPage.vue - app-frontend/src/layouts/AppLayout.vue - admin-frontend/src/pages/SettingsPage.vue - templates/register.html 3. 浏览器池统计API: - GET /yuyx/api/browser_pool/stats - 返回 worker 状态、队列等待数等信息 - browser_pool_worker.py: 增强 get_stats() 方法 4. 登录后支持 next 参数回跳: - app-frontend/src/pages/LoginPage.vue: 检查 ?next= 参数 - 仅允许站内路径(防止开放重定向) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,7 @@ class BrowserWorker(threading.Thread):
|
||||
self.total_tasks = 0
|
||||
self.failed_tasks = 0
|
||||
self.pre_warm = pre_warm
|
||||
self.last_activity_ts = 0.0
|
||||
|
||||
def log(self, message: str):
|
||||
"""日志输出"""
|
||||
@@ -63,35 +64,37 @@ class BrowserWorker(threading.Thread):
|
||||
else:
|
||||
print(f"[浏览器池][Worker-{self.worker_id}] {message}")
|
||||
|
||||
def _create_browser(self):
|
||||
"""创建浏览器实例"""
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
self.log("正在创建浏览器...")
|
||||
playwright = sync_playwright().start()
|
||||
browser = playwright.chromium.launch(
|
||||
headless=True,
|
||||
args=[
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
]
|
||||
)
|
||||
|
||||
self.browser_instance = {
|
||||
'playwright': playwright,
|
||||
'browser': browser,
|
||||
'created_at': time.time(),
|
||||
'use_count': 0,
|
||||
'worker_id': self.worker_id
|
||||
}
|
||||
self.log(f"浏览器创建成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"创建浏览器失败: {e}")
|
||||
def _create_browser(self):
|
||||
"""创建浏览器实例"""
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
self.log("正在创建浏览器...")
|
||||
playwright = sync_playwright().start()
|
||||
browser = playwright.chromium.launch(
|
||||
headless=True,
|
||||
args=[
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
]
|
||||
)
|
||||
|
||||
created_at = time.time()
|
||||
self.browser_instance = {
|
||||
'playwright': playwright,
|
||||
'browser': browser,
|
||||
'created_at': created_at,
|
||||
'use_count': 0,
|
||||
'worker_id': self.worker_id
|
||||
}
|
||||
self.last_activity_ts = created_at
|
||||
self.log(f"浏览器创建成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"创建浏览器失败: {e}")
|
||||
return False
|
||||
|
||||
def _close_browser(self):
|
||||
@@ -136,18 +139,15 @@ class BrowserWorker(threading.Thread):
|
||||
else:
|
||||
self.log("Worker启动(按需模式,等待任务时不占用浏览器资源)")
|
||||
|
||||
last_activity_time = 0
|
||||
if self.pre_warm and not self.browser_instance:
|
||||
if self._create_browser():
|
||||
last_activity_time = time.time()
|
||||
self._create_browser()
|
||||
self.pre_warm = False
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
# 允许运行中触发预热(例如池在初始化后调用 warmup)
|
||||
if self.pre_warm and not self.browser_instance:
|
||||
if self._create_browser():
|
||||
last_activity_time = time.time()
|
||||
self._create_browser()
|
||||
self.pre_warm = False
|
||||
|
||||
# 从队列获取任务(带超时,以便能响应停止信号和空闲检查)
|
||||
@@ -156,8 +156,8 @@ class BrowserWorker(threading.Thread):
|
||||
task = self.task_queue.get(timeout=TASK_QUEUE_TIMEOUT)
|
||||
except queue.Empty:
|
||||
# 检查是否需要关闭空闲的浏览器
|
||||
if self.browser_instance and last_activity_time > 0:
|
||||
idle_time = time.time() - last_activity_time
|
||||
if self.browser_instance and self.last_activity_ts > 0:
|
||||
idle_time = time.time() - self.last_activity_ts
|
||||
if idle_time > BROWSER_IDLE_TIMEOUT:
|
||||
self.log(f"空闲{int(idle_time)}秒,关闭浏览器释放资源")
|
||||
self._close_browser()
|
||||
@@ -192,13 +192,13 @@ class BrowserWorker(threading.Thread):
|
||||
result = task_func(self.browser_instance, *task_args, **task_kwargs)
|
||||
callback(result, None)
|
||||
self.log(f"任务执行成功")
|
||||
last_activity_time = time.time()
|
||||
self.last_activity_ts = time.time()
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"任务执行失败: {e}")
|
||||
callback(None, str(e))
|
||||
self.failed_tasks += 1
|
||||
last_activity_time = time.time()
|
||||
self.last_activity_ts = time.time()
|
||||
|
||||
# 任务失败后,检查浏览器健康
|
||||
if not self._check_browser_health():
|
||||
@@ -329,21 +329,47 @@ class BrowserWorkerPool:
|
||||
self.log(f"警告:任务队列已满(maxsize={self.task_queue.maxsize}),拒绝提交任务")
|
||||
return False
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""获取线程池统计信息"""
|
||||
idle_count = sum(1 for w in self.workers if w.idle)
|
||||
total_tasks = sum(w.total_tasks for w in self.workers)
|
||||
failed_tasks = sum(w.failed_tasks for w in self.workers)
|
||||
|
||||
return {
|
||||
'pool_size': self.pool_size,
|
||||
'idle_workers': idle_count,
|
||||
'busy_workers': self.pool_size - idle_count,
|
||||
'queue_size': self.task_queue.qsize(),
|
||||
'total_tasks': total_tasks,
|
||||
'failed_tasks': failed_tasks,
|
||||
'success_rate': f"{(total_tasks - failed_tasks) / total_tasks * 100:.1f}%" if total_tasks > 0 else "N/A"
|
||||
}
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""获取线程池统计信息"""
|
||||
workers = list(self.workers or [])
|
||||
idle_count = sum(1 for w in workers if getattr(w, "idle", False))
|
||||
total_tasks = sum(int(getattr(w, "total_tasks", 0) or 0) for w in workers)
|
||||
failed_tasks = sum(int(getattr(w, "failed_tasks", 0) or 0) for w in workers)
|
||||
|
||||
worker_details = []
|
||||
for w in workers:
|
||||
browser_instance = getattr(w, "browser_instance", None)
|
||||
browser_use_count = 0
|
||||
browser_created_at = None
|
||||
if isinstance(browser_instance, dict):
|
||||
browser_use_count = int(browser_instance.get("use_count", 0) or 0)
|
||||
browser_created_at = browser_instance.get("created_at")
|
||||
|
||||
worker_details.append(
|
||||
{
|
||||
"worker_id": getattr(w, "worker_id", None),
|
||||
"idle": bool(getattr(w, "idle", False)),
|
||||
"has_browser": bool(browser_instance),
|
||||
"total_tasks": int(getattr(w, "total_tasks", 0) or 0),
|
||||
"failed_tasks": int(getattr(w, "failed_tasks", 0) or 0),
|
||||
"browser_use_count": browser_use_count,
|
||||
"browser_created_at": browser_created_at,
|
||||
"last_active_ts": float(getattr(w, "last_activity_ts", 0) or 0),
|
||||
"thread_alive": bool(getattr(w, "is_alive", lambda: False)()),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
'pool_size': self.pool_size,
|
||||
'idle_workers': idle_count,
|
||||
'busy_workers': max(0, len(workers) - idle_count),
|
||||
'queue_size': self.task_queue.qsize(),
|
||||
'total_tasks': total_tasks,
|
||||
'failed_tasks': failed_tasks,
|
||||
'success_rate': f"{(total_tasks - failed_tasks) / total_tasks * 100:.1f}%" if total_tasks > 0 else "N/A",
|
||||
'workers': worker_details,
|
||||
'timestamp': time.time(),
|
||||
}
|
||||
|
||||
def wait_for_completion(self, timeout: Optional[float] = None):
|
||||
"""等待所有任务完成"""
|
||||
|
||||
Reference in New Issue
Block a user