| 时间 | 来源 | 类型 | 消息 | 路径 / 用户 | 状态 | +
|---|---|---|---|---|---|
| 加载中... | |||||
From abb70bbe3e704e5c5990de8351fce00811b49bc1 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sat, 16 May 2026 15:45:59 +0800 Subject: [PATCH] update --- app/cli.py | 13 +- app/db/auth_db.py | 8 +- app/db/migrations/0003_system_error_log.sql | 26 +++ app/db/system_logs.py | 209 ++++++++++++++++++++ app/web/routes_admin.py | 29 ++- app/web/routes_pages.py | 40 ++-- app/web/web_server.py | 29 +++ docker/scheduler.py | 11 ++ rules.yaml | 8 +- static/admin.html | 18 +- static/base.html | 29 ++- static/cron.html | 15 -- static/iteration.html | 16 -- static/llm_insights.html | 15 -- static/market.html | 15 -- static/onchain.html | 15 -- static/pipeline.html | 15 -- static/referral.html | 16 -- static/sentiment.html | 15 -- static/strategy.html | 15 -- static/subscription.html | 15 -- static/system_logs.html | 181 +++++++++++++++++ static/watchlist.html | 15 -- tests/test_system_error_logs.py | 89 +++++++++ 24 files changed, 636 insertions(+), 221 deletions(-) create mode 100644 app/db/migrations/0003_system_error_log.sql create mode 100644 app/db/system_logs.py create mode 100644 static/system_logs.html create mode 100644 tests/test_system_error_logs.py diff --git a/app/cli.py b/app/cli.py index bae848a..33f744a 100644 --- a/app/cli.py +++ b/app/cli.py @@ -1,6 +1,7 @@ """Unified CLI entrypoint for AlphaX Agent | Crypto jobs.""" import argparse +import sys from app.services import altcoin_confirm, altcoin_screener, event_driven_screener, onchain_monitor, price_tracker, review_engine, sentiment_monitor @@ -90,4 +91,14 @@ def main(): if __name__ == "__main__": - main() + try: + main() + except Exception as exc: + try: + from app.db.system_logs import record_exception + + command = " ".join(sys.argv[1:]) or "unknown" + record_exception(exc, source="cli", context={"argv": sys.argv, "command": command}) + except Exception: + pass + raise diff --git a/app/db/auth_db.py b/app/db/auth_db.py index c1a5457..cd27f59 100644 --- a/app/db/auth_db.py +++ b/app/db/auth_db.py @@ -712,11 +712,13 @@ def get_admin_stats(): """, (thirty_ago,)).fetchall() today_active = conn.execute(""" - SELECT DISTINCT u.id, u.email, u.created_at, u.last_login_at + SELECT u.id, u.email, u.created_at, u.last_login_at, MAX(ua.created_at) AS last_activity_at FROM user_activity ua JOIN app_user u ON u.id = ua.user_id WHERE ua.created_at LIKE %s - ORDER BY ua.created_at DESC LIMIT 20 + GROUP BY u.id, u.email, u.created_at, u.last_login_at + ORDER BY last_activity_at DESC + LIMIT 20 """, (today + "%",)).fetchall() conn.close() @@ -734,7 +736,7 @@ def get_admin_stats(): "pv_trend": [{"day": r[0], "count": r[1]} for r in pv_trend], "dau_trend": [{"day": r[0], "count": r[1]} for r in dau_trend], "today_active": [ - {"id": r[0], "email": r[1], "created_at": r[2], "last_login_at": r[3]} + {"id": r[0], "email": r[1], "created_at": r[2], "last_login_at": r[3], "last_activity_at": r[4]} for r in today_active ], } diff --git a/app/db/migrations/0003_system_error_log.sql b/app/db/migrations/0003_system_error_log.sql new file mode 100644 index 0000000..7c57ee0 --- /dev/null +++ b/app/db/migrations/0003_system_error_log.sql @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS system_error_log ( + id BIGSERIAL PRIMARY KEY, + created_at TEXT NOT NULL, + level TEXT DEFAULT 'error', + source TEXT DEFAULT 'app', + error_type TEXT DEFAULT '', + message TEXT DEFAULT '', + stack_trace TEXT DEFAULT '', + request_method TEXT DEFAULT '', + request_path TEXT DEFAULT '', + query_string TEXT DEFAULT '', + user_email TEXT DEFAULT '', + user_id BIGINT DEFAULT 0, + status_code INTEGER DEFAULT 0, + fingerprint TEXT DEFAULT '', + context_json TEXT DEFAULT '{}', + host TEXT DEFAULT '', + pid INTEGER DEFAULT 0, + resolved_at TEXT DEFAULT '', + resolved_by TEXT DEFAULT '', + resolution_note TEXT DEFAULT '' +); + +CREATE INDEX IF NOT EXISTS idx_system_error_log_created ON system_error_log(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_system_error_log_level_source ON system_error_log(level, source, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_system_error_log_fingerprint ON system_error_log(fingerprint, created_at DESC); diff --git a/app/db/system_logs.py b/app/db/system_logs.py new file mode 100644 index 0000000..7d4e7bf --- /dev/null +++ b/app/db/system_logs.py @@ -0,0 +1,209 @@ +"""System error log storage.""" + +from __future__ import annotations + +import hashlib +import json +import os +import socket +import traceback +from datetime import datetime, timedelta + +from app.db.schema import get_conn + + +def _now() -> str: + return datetime.now().isoformat() + + +def _truncate(value, limit: int) -> str: + text = str(value or "") + return text[:limit] + + +def _json(value) -> str: + try: + return json.dumps(value or {}, ensure_ascii=False, default=str) + except Exception: + return "{}" + + +def _fingerprint(error_type: str, message: str, stack_trace: str, path: str = "") -> str: + basis = "\n".join([ + str(error_type or ""), + str(message or "")[:500], + str(path or ""), + str(stack_trace or "").splitlines()[-12:][0] if stack_trace else "", + ]) + return hashlib.sha256(basis.encode("utf-8", errors="ignore")).hexdigest()[:32] + + +def record_system_error( + *, + source: str, + level: str = "error", + message: str = "", + error_type: str = "", + stack_trace: str = "", + request_method: str = "", + request_path: str = "", + query_string: str = "", + user_email: str = "", + user_id: int = 0, + status_code: int = 500, + context: dict | None = None, + fingerprint: str = "", +) -> int: + """Persist a system error. Logging must never raise into the caller.""" + try: + error_type = error_type or "Error" + stack_trace = stack_trace or "" + fingerprint = fingerprint or _fingerprint(error_type, message, stack_trace, request_path) + conn = get_conn() + try: + row = conn.execute( + """ + INSERT INTO system_error_log ( + created_at, level, source, error_type, message, stack_trace, + request_method, request_path, query_string, user_email, user_id, + status_code, fingerprint, context_json, host, pid + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + RETURNING id + """, + ( + _now(), + _truncate(level or "error", 20), + _truncate(source or "app", 80), + _truncate(error_type, 160), + _truncate(message, 2000), + _truncate(stack_trace, 60000), + _truncate(request_method, 16), + _truncate(request_path, 500), + _truncate(query_string, 1000), + _truncate(user_email, 255), + int(user_id or 0), + int(status_code or 0), + fingerprint, + _json(context), + _truncate(socket.gethostname(), 120), + int(os.getpid() or 0), + ), + ) + log_id = row.fetchone()["id"] + conn.commit() + return int(log_id) + finally: + conn.close() + except Exception: + return 0 + + +def record_exception(exc: BaseException, *, source: str, context: dict | None = None, **kwargs) -> int: + return record_system_error( + source=source, + error_type=exc.__class__.__name__, + message=str(exc), + stack_trace="".join(traceback.format_exception(type(exc), exc, exc.__traceback__)), + context=context or {}, + **kwargs, + ) + + +def list_system_errors( + *, + offset: int = 0, + limit: int = 50, + level: str = "", + source: str = "", + search: str = "", + hours: int = 168, +) -> dict: + limit = max(1, min(int(limit or 50), 200)) + offset = max(0, int(offset or 0)) + where = [] + params = [] + if level and level != "all": + where.append("level=%s") + params.append(level) + if source and source != "all": + where.append("source=%s") + params.append(source) + if search: + like = f"%{search.strip()}%" + where.append("(message ILIKE %s OR error_type ILIKE %s OR request_path ILIKE %s OR user_email ILIKE %s)") + params.extend([like, like, like, like]) + if hours and int(hours) > 0: + cutoff = (datetime.now() - timedelta(hours=int(hours))).isoformat() + where.append("created_at >= %s") + params.append(cutoff) + clause = "WHERE " + " AND ".join(where) if where else "" + conn = get_conn() + try: + total = conn.execute(f"SELECT COUNT(*) AS n FROM system_error_log {clause}", tuple(params)).fetchone()["n"] + rows = conn.execute( + f""" + SELECT id, created_at, level, source, error_type, message, request_method, + request_path, user_email, user_id, status_code, fingerprint, host, pid + FROM system_error_log + {clause} + ORDER BY id DESC + LIMIT %s OFFSET %s + """, + tuple(params + [limit, offset]), + ).fetchall() + return { + "items": [dict(row) for row in rows], + "total": int(total or 0), + "limit": limit, + "offset": offset, + "has_more": offset + limit < int(total or 0), + } + finally: + conn.close() + + +def get_system_error(log_id: int) -> dict | None: + conn = get_conn() + try: + row = conn.execute("SELECT * FROM system_error_log WHERE id=%s", (int(log_id),)).fetchone() + if not row: + return None + item = dict(row) + try: + item["context"] = json.loads(item.get("context_json") or "{}") + except Exception: + item["context"] = {} + return item + finally: + conn.close() + + +def get_system_error_stats(hours: int = 24) -> dict: + cutoff = (datetime.now() - timedelta(hours=int(hours or 24))).isoformat() + conn = get_conn() + try: + rows = conn.execute( + """ + SELECT level, source, COUNT(*) AS n + FROM system_error_log + WHERE created_at >= %s + GROUP BY level, source + ORDER BY n DESC + """, + (cutoff,), + ).fetchall() + latest = conn.execute( + """ + SELECT created_at, level, source, error_type, message + FROM system_error_log + ORDER BY id DESC LIMIT 1 + """ + ).fetchone() + return { + "hours": int(hours or 24), + "total": sum(int(row["n"] or 0) for row in rows), + "groups": [dict(row) for row in rows], + "latest": dict(latest) if latest else None, + } + finally: + conn.close() diff --git a/app/web/routes_admin.py b/app/web/routes_admin.py index 39a7608..a53dafb 100644 --- a/app/web/routes_admin.py +++ b/app/web/routes_admin.py @@ -10,6 +10,7 @@ from app.db.scheduler_db import ( set_job_enabled, set_job_interval, ) +from app.db.system_logs import get_system_error, get_system_error_stats, list_system_errors from app.web.shared import ( SchedulerIntervalRequest, SchedulerToggleRequest, @@ -29,7 +30,7 @@ def build_router(templates): require_admin(altcoin_session) except HTTPException as e: return HTMLResponse(content=f"
{e.detail}
返回看板", status_code=e.status_code) - return templates.TemplateResponse(request=request, name="admin.html", context={"show_nav": True}) + return templates.TemplateResponse(request=request, name="admin.html", context={"show_nav": True, "active_nav": "admin"}) @router.get("/api/admin/check") async def api_admin_check(altcoin_session: str = Cookie(default="")): @@ -54,6 +55,32 @@ def build_router(templates): require_admin(altcoin_session) return auth_db.get_admin_orders(search=search, offset=offset, limit=limit, status=status) + @router.get("/api/admin/system-errors") + async def api_admin_system_errors( + search: str = "", + offset: int = 0, + limit: int = 50, + level: str = "all", + source: str = "all", + hours: int = 168, + altcoin_session: str = Cookie(default=""), + ): + require_admin(altcoin_session) + return list_system_errors(search=search, offset=offset, limit=limit, level=level, source=source, hours=hours) + + @router.get("/api/admin/system-errors/stats") + async def api_admin_system_error_stats(hours: int = 24, altcoin_session: str = Cookie(default="")): + require_admin(altcoin_session) + return get_system_error_stats(hours=hours) + + @router.get("/api/admin/system-errors/{log_id}") + async def api_admin_system_error_detail(log_id: int, altcoin_session: str = Cookie(default="")): + require_admin(altcoin_session) + item = get_system_error(log_id) + if not item: + raise HTTPException(status_code=404, detail="日志不存在") + return item + @router.get("/api/scheduler/jobs") async def api_scheduler_jobs(altcoin_session: str = Cookie(default="")): require_admin(altcoin_session) diff --git a/app/web/routes_pages.py b/app/web/routes_pages.py index 53c0615..e481761 100644 --- a/app/web/routes_pages.py +++ b/app/web/routes_pages.py @@ -10,7 +10,7 @@ from app.web.shared import require_admin, require_page_user def build_router(templates, repo_root: Path, stock_report_template: str): router = APIRouter() - def render_page(template_name: str, request: Request, **kwargs): + def render_page(template_name: str, request: Request, active_nav: str = "", **kwargs): try: user = auth_db.get_user_by_session_token(request.cookies.get("altcoin_session", "")) if user: @@ -22,7 +22,8 @@ def build_router(templates, repo_root: Path, stock_report_template: str): ) except Exception: pass - return templates.TemplateResponse(request=request, name=template_name, context={"show_nav": True, **kwargs}) + nav = active_nav or template_name.replace(".html", "").replace("-", "_") + return templates.TemplateResponse(request=request, name=template_name, context={"show_nav": True, "active_nav": nav, **kwargs}) @router.get("/", response_class=HTMLResponse) async def index(): @@ -39,21 +40,21 @@ def build_router(templates, repo_root: Path, stock_report_template: str): user, redirect = require_page_user(request) if redirect: return redirect - return render_page("watchlist.html", request) + return render_page("watchlist.html", request, active_nav="watchlist") @router.get("/pipeline", response_class=HTMLResponse) async def pipeline_page(request: Request): user, redirect = require_page_user(request) if redirect: return redirect - return render_page("pipeline.html", request) + return render_page("pipeline.html", request, active_nav="pipeline") @router.get("/llm-insights", response_class=HTMLResponse) async def llm_insights_page(request: Request): user, redirect = require_page_user(request) if redirect: return redirect - return render_page("llm_insights.html", request) + return render_page("llm_insights.html", request, active_nav="llm_insights") @router.get("/cron", response_class=HTMLResponse) async def cron_page(request: Request): @@ -64,28 +65,39 @@ def build_router(templates, repo_root: Path, stock_report_template: str): require_admin(request.cookies.get("altcoin_session", "")) except HTTPException as exc: return HTMLResponse(content=f"{exc.detail}
返回看板", status_code=exc.status_code) - return render_page("cron.html", request) + return render_page("cron.html", request, active_nav="cron") + + @router.get("/system-logs", response_class=HTMLResponse) + async def system_logs_page(request: Request): + user, redirect = require_page_user(request) + if redirect: + return redirect + try: + require_admin(request.cookies.get("altcoin_session", "")) + except HTTPException as exc: + return HTMLResponse(content=f"{exc.detail}
返回看板", status_code=exc.status_code) + return render_page("system_logs.html", request, active_nav="system_logs") @router.get("/strategy", response_class=HTMLResponse) async def strategy_page(request: Request): user, redirect = require_page_user(request) if redirect: return redirect - return render_page("strategy.html", request) + return render_page("strategy.html", request, active_nav="strategy") @router.get("/subscription", response_class=HTMLResponse) async def subscription_page(request: Request): user, redirect = require_page_user(request, require_subscription=False) if redirect: return redirect - return render_page("subscription.html", request) + return render_page("subscription.html", request, active_nav="subscription") @router.get("/referral", response_class=HTMLResponse) async def referral_page(request: Request, altcoin_session: str = Cookie(default="")): user, redirect = require_page_user(request) if redirect: return redirect - return render_page("referral.html", request) + return render_page("referral.html", request, active_nav="referral") @router.get("/app", response_class=HTMLResponse) async def app_page(altcoin_session: str = Cookie(default=""), request: Request = None): @@ -96,7 +108,7 @@ def build_router(templates, repo_root: Path, stock_report_template: str): auth_db.log_user_activity(user["id"], "page_view", "app", ip=request.client.host if request and request.client else "") except Exception: pass - resp = templates.TemplateResponse(request=request, name="app.html", context={"show_nav": True}) + resp = templates.TemplateResponse(request=request, name="app.html", context={"show_nav": True, "active_nav": "app"}) resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" resp.headers["Pragma"] = "no-cache" resp.headers["Expires"] = "0" @@ -107,28 +119,28 @@ def build_router(templates, repo_root: Path, stock_report_template: str): user, redirect = require_page_user(request) if redirect: return redirect - return render_page("market.html", request) + return render_page("market.html", request, active_nav="market") @router.get("/sentiment", response_class=HTMLResponse) async def sentiment_page(request: Request): user, redirect = require_page_user(request) if redirect: return redirect - return render_page("sentiment.html", request) + return render_page("sentiment.html", request, active_nav="sentiment") @router.get("/onchain", response_class=HTMLResponse) async def onchain_page(request: Request): user, redirect = require_page_user(request) if redirect: return redirect - return render_page("onchain.html", request) + return render_page("onchain.html", request, active_nav="onchain") @router.get("/iteration", response_class=HTMLResponse) async def iteration_page(request: Request): user, redirect = require_page_user(request) if redirect: return redirect - return render_page("iteration.html", request) + return render_page("iteration.html", request, active_nav="iteration") @router.get("/stock-report", response_class=HTMLResponse) async def stock_report_page(): diff --git a/app/web/web_server.py b/app/web/web_server.py index 3f1bebc..d2f01b3 100644 --- a/app/web/web_server.py +++ b/app/web/web_server.py @@ -6,10 +6,12 @@ from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse from fastapi.templating import Jinja2Templates from app.db.schema import init_db from app.db import auth_db +from app.db.system_logs import record_exception from app.db.analytics import get_all_recommendations, get_cron_run_logs, get_cron_run_summary, get_review_stats, get_stats from app.db.recommendation_queries import get_active_recommendations, get_active_recommendations_deduped from app.web.routes_admin import build_router as build_admin_router @@ -57,3 +59,30 @@ async def bind_current_request(request: Request, call_next): return await call_next(request) finally: current_request.reset(token) + + +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + user = None + try: + user = auth_db.get_user_by_session_token(request.cookies.get("altcoin_session", "")) + except Exception: + user = None + log_id = record_exception( + exc, + source="web", + request_method=request.method, + request_path=request.url.path, + query_string=request.url.query, + user_email=(user or {}).get("email", ""), + user_id=(user or {}).get("id", 0), + status_code=500, + context={ + "client": request.client.host if request.client else "", + "user_agent": request.headers.get("user-agent", ""), + }, + ) + return JSONResponse( + status_code=500, + content={"detail": "系统内部错误", "error_id": log_id}, + ) diff --git a/docker/scheduler.py b/docker/scheduler.py index 28f4230..0be4737 100755 --- a/docker/scheduler.py +++ b/docker/scheduler.py @@ -24,6 +24,7 @@ from app.db.scheduler_db import ( update_manual_trigger, update_runtime, ) +from app.db.system_logs import record_system_error PYTHON = sys.executable DRY_RUN = os.getenv("ALPHAX_SCHEDULER_DRY_RUN", "1").strip() not in {"0", "false", "False", "no", "NO"} @@ -224,6 +225,16 @@ def finish_running_jobs(running: dict[str, RunningJob]) -> None: print(f"[{now_str()}] [scheduler] done {name} exit={exit_code} duration={duration_ms/1000:.1f}s", flush=True) if output_tail: print(output_tail, flush=True) + if exit_code != 0: + record_system_error( + source="scheduler", + level="error", + error_type=f"{name}_exit_{exit_code}", + message=f"scheduler job {name} failed with exit={exit_code}", + stack_trace=output_tail, + status_code=exit_code, + context={"job_name": name, "run_kind": item.run_kind, "trigger_id": item.trigger_id}, + ) update_runtime( name, status=status, diff --git a/rules.yaml b/rules.yaml index d859f24..2ae8fd1 100644 --- a/rules.yaml +++ b/rules.yaml @@ -407,11 +407,11 @@ event_driven: note: Solana meme主题扩散 meta: version: 1 - last_review: '2026-05-16T11:57:01.719794' - last_reverse_analysis: '2026-05-16T11:57:32.488216' - total_reviews: 52 + last_review: '2026-05-16T15:25:43.236681' + last_reverse_analysis: '2026-05-16T15:27:11.686080' + total_reviews: 53 total_rules_learned: 37 - iteration_count: 57 + iteration_count: 58 strategy_version: v1.7.11 strategy_revision_started_at: '2026-05-09T01:20:00' strategy_revision_note: 'v1.7.11: 触发时效治理,旧形态只作背景,消息触发显式标记' diff --git a/static/admin.html b/static/admin.html index 778981b..96b5660 100644 --- a/static/admin.html +++ b/static/admin.html @@ -1,22 +1,6 @@ {% extends "base.html" %} {% block title %}管理看板 · AlphaX Agent | Crypto{% endblock %} -{% block nav_links %} -看板 -市场总览 -舆情 -链上异动 -订阅 -推荐 - -链路日志 -调度中心 -AI 记录 -策略 -迭代 -管理 -{% endblock %} - {% block extra_head_css %} +{% endblock %} + +{% block content %} +| 时间 | 来源 | 类型 | 消息 | 路径 / 用户 | 状态 | +
|---|---|---|---|---|---|
| 加载中... | |||||