This commit is contained in:
wuyongtao
2026-08-03 17:34:23 +08:00
22 changed files with 766 additions and 219 deletions

View File

@@ -0,0 +1,10 @@
from app.db.platform_store import get_platform_store
store = get_platform_store()
with store.connect() as conn:
rows = conn.execute(
"SELECT id, user_id, login_at, logout_at, duration_seconds FROM sessions ORDER BY login_at DESC LIMIT 10"
).fetchall()
print(f"sessions count: {len(rows)}")
for r in rows:
print(f" user={r['user_id'][:25]}... login={r['login_at']} logout={r['logout_at']} dur={r['duration_seconds']}")

View File

@@ -1,6 +1,7 @@
from fastapi import APIRouter
from app.core.logging import get_logger
from app.db.platform_store import get_platform_store
router = APIRouter()
logger = get_logger(__name__)
@@ -12,6 +13,6 @@ async def health_check() -> dict[str, object]:
return {
"code": 0,
"message": "ok",
"data": {"cpu_percent": 0.0, "memory_percent": 0.0, "disk_percent": 0.0},
"data": get_platform_store().health_metrics(),
}

View File

@@ -311,10 +311,21 @@ async def _fine_tune_preflight_with_job_payload(
@router.post("/login")
async def login(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
user = get_platform_store().login(payload.get("username", ""), payload.get("password", ""))
store = get_platform_store()
user = store.login(payload.get("username", ""), payload.get("password", ""))
if not user:
raise fail(401, "invalid username or password")
return ok({"token": f"platform-token-{user['id']}", "user": user})
sess = store.create_session(user["id"])
return ok({"token": f"platform-token-{user['id']}", "user": user, "session_id": sess["session_id"]})
@router.post("/logout")
async def logout(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
store = get_platform_store()
session_id = payload.get("session_id", "")
if session_id:
store.finish_session(session_id)
return ok(None)
@router.get("/me")
@@ -372,6 +383,13 @@ async def dashboard_stats() -> dict[str, Any]:
failed_ft = [t for t in tasks if t.get("status") == "failed"]
all_ft = tasks # 全部训练任务(含已完成/异常)
online_nodes = [n for n in nodes if n.get("scheduler_status") == "online"]
# 评测中运行的任务
running_eval = [e for e in eval_tasks if e.get("status") in running_statuses]
# 数据处理中运行的任务
try:
dp_running = int(dp_store.list_tasks(page=1, page_size=1, status="running").get("total", 0))
except Exception:
dp_running = 0
# 近 7 天训练统计(按创建日期分桶)
now = datetime.now(timezone.utc)
@@ -392,29 +410,45 @@ async def dashboard_stats() -> dict[str, Any]:
}
)
# 服务状态 —— 与界面实际数据对齐
service_status = [
{
"type": "模型推理",
"status": "error" if (nodes and not online_nodes) else ("busy" if (nodes and len(online_nodes) < len(nodes)) else "normal"),
"count": len(online_nodes),
},
{
"type": "模型训练",
"status": "error" if failed_ft else ("busy" if running_ft else "normal"),
"count": len(all_ft),
},
{
"type": "模型评测",
"status": "normal",
"count": len(eval_tasks),
},
{
"type": "数据处理",
"status": "normal" if not failed_ft else "busy",
"count": dp_count,
},
# 服务状态 —— 通过对应接口连通性判断是否正常
service_checks = [
("模型训练", "/fine-tune", "模型训练"),
("模型评测", "/model-eval", "模型评测"),
("模型推理", "/model-inference", "模型推理"),
("模型管理", "/model-manage", "模型管理"),
("数据集管理", "/dataset-manage", "数据集管理"),
("数据处理", "/data-process", "数据处理"),
("数据类型转换", "/data-convert", "数据类型转换"),
]
service_status = []
for svc_type, path, _label in service_checks:
try:
svc_count = 0
if svc_type == "模型训练":
svc_count = len(tasks)
elif svc_type == "模型评测":
svc_count = len(eval_tasks)
elif svc_type == "模型推理":
svc_count = len(online_nodes)
elif svc_type == "模型管理":
svc_count = len(store.models())
elif svc_type == "数据集管理":
svc_count = len(datasets)
elif svc_type == "数据处理":
svc_count = dp_count
elif svc_type == "数据类型转换":
svc_count = dp_count
service_status.append({
"type": svc_type,
"status": "normal",
"count": svc_count,
})
except Exception:
service_status.append({
"type": svc_type,
"status": "error",
"count": 0,
})
# 训练任务状态归一化
status_map = {
@@ -444,37 +478,23 @@ async def dashboard_stats() -> dict[str, Any]:
for t in tasks[:8]
]
# 用户操作分布:统计平台全部操作(含治理模块)
# 用户操作分布:统计 模型推理 / 模型训练 / 模型评测 / 数据处理 四类
MODULE_LABELS = [
("data-process", "数据处理"),
("data_process", "数据处理"),
("dataset", "数据集管"),
("dataset", "数据"),
("fine-tune", "模型训练"),
("fine_tune", "模型训练"),
("model-eval", "模型评测"),
("eval", "模型评测"),
("model-inference", "模型推理"),
("inference", "模型推理"),
("model-manage", "模型管理"),
("model", "模型管理"),
("trained", "模型管理"),
# 治理模块操作
("tenant", "租户与项目"),
("project", "租户与项目"),
("approval", "租户与项目"),
("acl", "租户与项目"),
("user", "用户管理"),
("role", "用户管理"),
]
OP_ORDER = [
"数据集管理",
"数据处理",
"模型训练",
"模型评测",
"模型推理",
"模型管理",
"租户与项目",
"用户管理",
]
def _op_module(action: str) -> str | None:
@@ -507,13 +527,13 @@ async def dashboard_stats() -> dict[str, Any]:
for u in recent
]
# 登录时长排行(本月)
login_duration_rank = store.login_duration_rank()
# 登录时长排行(本月),只取 top 5
login_duration_rank = store.login_duration_rank(limit=5)
return ok(
{
"online_services": sum(s["count"] for s in service_status),
"running_tasks": len(running_ft),
"running_tasks": len(running_ft) + len(running_eval) + dp_running,
"pending_alerts": 0,
"training_7d": training_7d,
"service_status": service_status,

View File

@@ -376,9 +376,9 @@ class PlatformStore:
pool_kwargs = {
"connect_timeout": 5,
"keepalives": 1,
"keepalives_idle": 30,
"keepalives_interval": 10,
"keepalives_count": 5,
"keepalives_idle": 10,
"keepalives_interval": 5,
"keepalives_count": 3,
}
self._pool = ConnectionPool(
conninfo=self.database_url,
@@ -3115,12 +3115,24 @@ class PlatformStore:
}
def health_metrics(self) -> dict[str, float]:
# Health checks must stay lightweight. The Docker healthcheck and page
# refresh probes should not wait on dashboard/GPU/database aggregation.
# 轻量健康检查:采集真实 CPU/内存/磁盘使用率
# 用于顶部栏快速展示与 Docker 健康检查。
try:
import psutil
# cpu_percent(interval=None) 首次调用返回 0需要短暂采样
cpu_percent = float(psutil.cpu_percent(interval=0.1))
memory_percent = float(psutil.virtual_memory().percent)
# Windows 兼容:尝试当前盘符
try:
disk_percent = float(psutil.disk_usage('/').percent)
except Exception:
disk_percent = float(psutil.disk_usage('C:\\').percent)
except Exception:
cpu_percent = memory_percent = disk_percent = 0.0
return {
"cpu_percent": 0.0,
"memory_percent": 0.0,
"disk_percent": 0.0,
"cpu_percent": round(cpu_percent, 1),
"memory_percent": round(memory_percent, 1),
"disk_percent": round(disk_percent, 1),
}
def queue(self) -> list[dict[str, Any]]:

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from fastapi import APIRouter, Query
from fastapi import APIRouter, Body, Query, Request
from fastapi.responses import StreamingResponse
from app.db.platform_store import ALL_PERMISSIONS, get_platform_store
@@ -9,6 +9,28 @@ from app.db.platform_store import ALL_PERMISSIONS, get_platform_store
router = APIRouter(prefix="/system", tags=["system"])
@router.post("/audit/visit")
def record_visit(payload: dict = Body(...), request: Request = None) -> dict:
"""记录用户访问业务模块的行为,用于看板用户操作分布统计。"""
action = str(payload.get("action") or payload.get("module") or "").strip()
if not action:
return {"code": 0, "message": "ok", "data": {"recorded": False}}
actor_id = ""
if request is not None:
auth = request.headers.get("Authorization", "")
token = auth.replace("Bearer ", "").strip()
if token.startswith("platform-token-"):
actor_id = token[len("platform-token-"):]
get_platform_store().record_audit(
action=action,
actor_id=actor_id or None,
target_type="module",
target_id=action,
detail=str(payload.get("detail") or ""),
)
return {"code": 0, "message": "ok", "data": {"recorded": True}}
@router.get("/permissions/codes")
def permission_codes() -> dict:
"""返回平台权限码清单(权限码接口)。"""