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:
2025-12-27 18:28:21 +08:00
parent 3d9dba272e
commit 1b20478a08
49 changed files with 305 additions and 160 deletions

View File

@@ -317,6 +317,71 @@ def get_system_stats():
return jsonify(stats)
@admin_api_bp.route("/browser_pool/stats", methods=["GET"])
@admin_required
def get_browser_pool_stats():
"""获取浏览器池状态"""
try:
from browser_pool_worker import get_browser_worker_pool
pool = get_browser_worker_pool()
stats = pool.get_stats() or {}
worker_details = []
for w in stats.get("workers") or []:
last_ts = float(w.get("last_active_ts") or 0)
last_active_at = None
if last_ts > 0:
try:
last_active_at = datetime.fromtimestamp(last_ts, tz=BEIJING_TZ).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
last_active_at = None
created_ts = w.get("browser_created_at")
created_at = None
if created_ts:
try:
created_at = datetime.fromtimestamp(float(created_ts), tz=BEIJING_TZ).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
created_at = None
worker_details.append(
{
"worker_id": w.get("worker_id"),
"idle": bool(w.get("idle")),
"has_browser": bool(w.get("has_browser")),
"total_tasks": int(w.get("total_tasks") or 0),
"failed_tasks": int(w.get("failed_tasks") or 0),
"browser_use_count": int(w.get("browser_use_count") or 0),
"browser_created_at": created_at,
"browser_created_ts": created_ts,
"last_active_at": last_active_at,
"last_active_ts": last_ts,
"thread_alive": bool(w.get("thread_alive")),
}
)
total_workers = len(worker_details) if worker_details else int(stats.get("pool_size") or 0)
return jsonify(
{
"total_workers": total_workers,
"active_workers": int(stats.get("busy_workers") or 0),
"idle_workers": int(stats.get("idle_workers") or 0),
"queue_size": int(stats.get("queue_size") or 0),
"workers": worker_details,
"summary": {
"total_tasks": int(stats.get("total_tasks") or 0),
"failed_tasks": int(stats.get("failed_tasks") or 0),
"success_rate": stats.get("success_rate"),
},
"server_time_cst": get_beijing_now().strftime("%Y-%m-%d %H:%M:%S"),
}
)
except Exception as e:
logger.exception(f"[AdminAPI] 获取浏览器池状态失败: {e}")
return jsonify({"error": "获取浏览器池状态失败"}), 500
@admin_api_bp.route("/docker_stats", methods=["GET"])
@admin_required
def get_docker_stats():