alphax/app/web/routes_admin.py
2026-06-03 23:46:01 +08:00

209 lines
10 KiB
Python

from fastapi import APIRouter, Cookie, HTTPException, Request
from fastapi.responses import HTMLResponse, Response
from app.config.system_config import seed_runtime_system_defaults
from app.db import auth_db
from app.db import chat_assistant_db
from app.db.analytics import get_cron_run_logs, get_cron_run_summary, get_pipeline_runs
from app.db.data_export import build_data_export_bundle
from app.db.operations_dashboard import get_operations_dashboard
from app.db.scheduler_db import (
enqueue_manual_trigger,
get_job_config,
get_scheduler_overview,
list_manual_triggers,
set_job_enabled,
set_job_interval,
)
from app.db.runtime_config_db import delete_config, get_config, list_configs, set_config
from app.db.system_logs import get_system_error, get_system_error_stats, list_system_errors
from app.web.shared import (
RuntimeConfigRequest,
SchedulerIntervalRequest,
SchedulerToggleRequest,
SchedulerTriggerRequest,
login_redirect,
require_admin,
)
def build_router(templates):
router = APIRouter()
@router.get("/admin.html", response_class=HTMLResponse)
async def admin_page(request: Request, altcoin_session: str = Cookie(default="")):
if not auth_db.get_user_by_session_token(altcoin_session):
return login_redirect()
try:
require_admin(altcoin_session)
except HTTPException as e:
return HTMLResponse(content=f"<meta charset=utf-8><h2>需要管理员权限</h2><p>{e.detail}</p><a href=/app>返回看板</a>", status_code=e.status_code)
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="")):
try:
user = require_admin(altcoin_session)
return {"is_admin": True, "email": user.get("email", "")}
except HTTPException:
return {"is_admin": False}
@router.get("/api/admin/stats")
async def api_admin_stats(altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
return auth_db.get_admin_stats()
@router.get("/api/admin/operations-dashboard")
async def api_admin_operations_dashboard(hours: int = 24, altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
return get_operations_dashboard(hours=hours)
@router.get("/api/admin/users")
async def api_admin_users(search: str = "", offset: int = 0, limit: int = 50, tab: str = "all", altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
return auth_db.get_admin_users(search=search, offset=offset, limit=limit, tab=tab)
@router.get("/api/admin/orders")
async def api_admin_orders(search: str = "", offset: int = 0, limit: int = 50, status: str = "all", altcoin_session: str = Cookie(default="")):
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/admin/cron-runs")
async def api_admin_cron_runs(limit: int = 80, job_name: str = "", altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
return {"items": get_cron_run_logs(limit=limit, job_name=job_name or None)}
@router.get("/api/admin/cron-runs/summary")
async def api_admin_cron_run_summary(hours: int = 24, altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
return get_cron_run_summary(hours=hours)
@router.get("/api/admin/pipeline-runs")
async def api_admin_pipeline_runs(limit: int = 30, hours: int = 24, offset: int = 0, altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
return get_pipeline_runs(limit=limit, hours=hours, offset=offset)
@router.get("/api/admin/chat-logs/overview")
async def api_admin_chat_logs_overview(hours: int = 24, altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
return chat_assistant_db.get_chat_admin_overview(hours=hours)
@router.get("/api/admin/chat-logs")
async def api_admin_chat_logs(
hours: int = 24,
intent: str = "all",
search: str = "",
offset: int = 0,
limit: int = 50,
altcoin_session: str = Cookie(default=""),
):
require_admin(altcoin_session)
return chat_assistant_db.list_chat_admin_questions(hours=hours, intent=intent, search=search, offset=offset, limit=limit)
@router.get("/api/admin/data-export")
async def api_admin_data_export(hours: int = 24, altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
filename, content, _manifest = build_data_export_bundle(hours=hours)
return Response(
content=content,
media_type="application/zip",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
@router.get("/api/runtime-config")
async def api_runtime_config(kind: str = "all", altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
if kind in ("all", "system"):
seed_runtime_system_defaults()
if kind == "strategy":
return {"items": list_configs("strategy")}
if kind == "system":
return {"items": list_configs("system")}
return {"items": list_configs("strategy") + list_configs("system")}
@router.get("/api/runtime-config/{kind}/{config_key:path}")
async def api_runtime_config_detail(kind: str, config_key: str, altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
if kind not in ("strategy", "system"):
raise HTTPException(status_code=400, detail="kind must be strategy or system")
return {"kind": kind, "config_key": config_key, "config": get_config(kind, config_key, default={})}
@router.put("/api/runtime-config/{kind}/{config_key:path}")
async def api_runtime_config_update(kind: str, config_key: str, payload: RuntimeConfigRequest, altcoin_session: str = Cookie(default="")):
user = require_admin(altcoin_session)
if kind not in ("strategy", "system"):
raise HTTPException(status_code=400, detail="kind must be strategy or system")
config = set_config(kind, config_key, payload.config, description=payload.description, source="admin_page", updated_by=user.get("email", ""))
return {"ok": True, "kind": kind, "config_key": config_key, "config": config}
@router.delete("/api/runtime-config/{kind}/{config_key:path}")
async def api_runtime_config_delete(kind: str, config_key: str, altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
if kind not in ("strategy", "system"):
raise HTTPException(status_code=400, detail="kind must be strategy or system")
return {"ok": delete_config(kind, config_key), "kind": kind, "config_key": config_key}
@router.get("/api/scheduler/jobs")
async def api_scheduler_jobs(altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
return get_scheduler_overview()
@router.post("/api/scheduler/jobs/{job_name}/toggle")
async def api_scheduler_toggle(job_name: str, payload: SchedulerToggleRequest, altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
if not set_job_enabled(job_name, payload.enabled):
raise HTTPException(status_code=404, detail="任务不存在")
return {"ok": True, "job_name": job_name, "enabled": payload.enabled}
@router.post("/api/scheduler/jobs/{job_name}/interval")
async def api_scheduler_interval(job_name: str, payload: SchedulerIntervalRequest, altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
if payload.every_seconds < 30:
raise HTTPException(status_code=400, detail="周期不能低于 30 秒")
if not set_job_interval(job_name, payload.every_seconds):
raise HTTPException(status_code=404, detail="任务不存在")
return {"ok": True, "job_name": job_name, "every_seconds": payload.every_seconds}
@router.post("/api/scheduler/jobs/{job_name}/trigger")
async def api_scheduler_trigger(job_name: str, payload: SchedulerTriggerRequest, altcoin_session: str = Cookie(default="")):
user = require_admin(altcoin_session)
job = get_job_config(job_name)
if not job:
raise HTTPException(status_code=404, detail="任务不存在")
if not job.get("enabled") and not payload.force:
raise HTTPException(status_code=409, detail="任务已关闭,需要确认后 force=true 才能单次运行")
trigger_id = enqueue_manual_trigger(job_name, force=payload.force, requested_by=user.get("email", ""))
return {"ok": True, "job_name": job_name, "trigger_id": trigger_id, "force": payload.force}
@router.get("/api/scheduler/triggers")
async def api_scheduler_triggers(limit: int = 30, altcoin_session: str = Cookie(default="")):
require_admin(altcoin_session)
return {"items": list_manual_triggers(limit=limit)}
return router