diff --git a/backend/app/api/v1/endpoints/health.py b/backend/app/api/v1/endpoints/health.py index 8a01289..55db91b 100644 --- a/backend/app/api/v1/endpoints/health.py +++ b/backend/app/api/v1/endpoints/health.py @@ -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(), } diff --git a/backend/app/api/v1/endpoints/platform.py b/backend/app/api/v1/endpoints/platform.py index c676e88..5b36a2e 100644 --- a/backend/app/api/v1/endpoints/platform.py +++ b/backend/app/api/v1/endpoints/platform.py @@ -383,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) @@ -403,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 = { @@ -510,7 +533,7 @@ async def dashboard_stats() -> dict[str, Any]: 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, diff --git a/backend/app/db/platform_store.py b/backend/app/db/platform_store.py index 7cad6af..cb0580d 100644 --- a/backend/app/db/platform_store.py +++ b/backend/app/db/platform_store.py @@ -2996,12 +2996,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]]: diff --git a/backend/app/modules/system/router.py b/backend/app/modules/system/router.py index a95d144..11c17c5 100644 --- a/backend/app/modules/system/router.py +++ b/backend/app/modules/system/router.py @@ -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: """返回平台权限码清单(权限码接口)。""" diff --git a/frontend/src/api/modules/audit-visit.ts b/frontend/src/api/modules/audit-visit.ts new file mode 100644 index 0000000..5f58b3f --- /dev/null +++ b/frontend/src/api/modules/audit-visit.ts @@ -0,0 +1,5 @@ +import { post } from '../request' + +/** 记录用户访问某个业务模块(用于看板用户操作分布统计) */ +export const recordModuleVisit = (module: string, detail?: string) => + post('/system/audit/visit', { action: module, detail: detail || '' }) \ No newline at end of file diff --git a/frontend/src/api/request.ts b/frontend/src/api/request.ts index dcd54d6..bc9afa9 100644 --- a/frontend/src/api/request.ts +++ b/frontend/src/api/request.ts @@ -1,6 +1,37 @@ import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios' import { ElMessage } from 'element-plus' +/** + * 用户操作分布:哪些模块路径算"业务操作"(用于看板统计) + * 请求命中这些路径时,会自动调用 record_visit 记录一次(同一模块 60 秒内去重) + */ +const VISIT_TRACKED_PREFIXES: Array<[string, string]> = [ + ['/fine-tune', 'fine-tune'], + ['/model-eval', 'model-eval'], + ['/model-inference', 'model-inference'], + ['/data-process', 'data-process'], + ['/data-convert', 'data-convert'], + ['/model-manage', 'model-manage'], + ['/dataset-manage', 'dataset'], +] + +function trackVisit(url: string | undefined) { + if (!url) return + for (const [prefix, module] of VISIT_TRACKED_PREFIXES) { + if (url.includes(prefix)) { + const key = `visit:${module}` + const last = Number(sessionStorage.getItem(key) || 0) + if (Date.now() - last < 60000) return // 60 秒内去重 + sessionStorage.setItem(key, String(Date.now())) + // fire-and-forget 调用后端记录接口 + import('./modules/audit-visit').then(({ recordModuleVisit }) => { + recordModuleVisit(module, url).catch(() => { /* ignore */ }) + }).catch(() => { /* ignore */ }) + return + } + } +} + /** * 后端统一响应格式 * code === 0 表示成功,data 为业务数据 @@ -60,6 +91,8 @@ service.interceptors.response.use( return response } if (res.code === 0) { + // 记录业务模块访问(用于看板用户操作分布统计) + trackVisit(response.config.url) return res.data } // 业务错误 diff --git a/平台治理.md b/平台治理.md new file mode 100644 index 0000000..58379e5 --- /dev/null +++ b/平台治理.md @@ -0,0 +1,282 @@ +# 平台治理功能说明 + +平台治理涵盖用户管理、租户管理、项目管理、审批管理、审计日志、资源授权(ACL)、留存策略等企业管理能力。功能入口位于侧边栏「平台治理」和「系统设置」两个分组下。 + +本平台核心业务是**模型微调**:用户上传数据 → 数据处理 → 模型训练 → 模型评测 → 模型推理。平台治理负责管理**谁**能访问**哪个租户/项目**的**哪些资源**。 + +--- + +## 侧边栏菜单结构 + +``` +平台治理 +├── 租户管理 /tenants +├── 项目空间 /projects +├── 审计日志 /audit-logs +├── 审批模板 /approval-templates +└── 审批中心 /approval-instances + +系统设置 +├── 用户设置 /user-settings +├── 平台性能 /hardware +└── 查看日志 /logs +``` + +--- + +## 1. 租户管理 + +**对应页面**:列表页 `TenantListView.vue` + 详情页 `TenantDetailView.vue` +**后端接口**:`GET/POST /tenants`、`GET/PUT/DELETE /tenants/:id`、`PUT /tenants/:id/quota`、`PUT /tenants/:id/retention-policy` + +### 列表页 + +**展示列**:租户名称、用户ID、状态、创建时间 + +**行操作**: +| 按钮 | 功能 | +|------|------| +| 详情 | 跳转详情页 `/tenants/:id` | +| 删除 | 确认弹窗后调用 `DELETE /tenants/:id` | + +**新建租户弹窗**: +| 字段 | 控件 | 说明 | +|------|------|------| +| 名称 | 文本输入 | 必填 | +| 用户ID | 文本输入 | 可选 | +| GPU 数量 | 数字输入 | 配额,0=不限制 | +| 存储配额(GB) | 数字输入 | 配额,0=不限制 | +| 最大项目数 | 数字输入 | 配额,0=不限制 | + +### 详情页 + +**基本信息**:名称、用户ID、状态、创建时间、配额(友好格式显示,如 `GPU 8 | 存储 100GB | 项目 5`) + +**配额编辑**:GPU 数量 / 存储配额(GB) / 最大项目数,三个数字输入 + 保存按钮 + +**已移除**:详情页中不再内嵌项目空间列表(项目有独立页面) + +### 当前缺失 +- [ ] 编辑租户名称和用户ID(`updateTenant` API 已有,UI 缺失) +- [ ] 留存策略关联设置(`setTenantRetention` API 已有,UI 缺失) +- [ ] 列表页无搜索/筛选 +- [ ] 状态列无 Tag 着色 + +--- + +## 2. 项目空间 + +**对应页面**:列表页 `ProjectListView.vue` + 详情页 `ProjectDetailView.vue` +**后端接口**:`GET/POST /projects`、`GET/PUT/DELETE /projects/:id`、`GET/POST/DELETE /projects/:id/members`、`PUT /projects/:id/archive` + +### 列表页 + +**展示列**:项目名、编码ID、状态、描述、创建时间 + +**工具栏**: +- 租户选择器(下拉过滤,按租户编码选择) +- 搜索框(按项目名/编码搜索) +- 新建项目按钮 + +**新建项目弹窗**: +| 字段 | 控件 | 说明 | +|------|------|------| +| 名称 | 文本输入 | 必填 | +| 编码ID | 下拉选择 | 选择已有租户的编码(如 `default`),关联到该租户 | +| 描述 | 文本域 | 可选,3行 | + +**行操作**:详情 + 删除 + +### 详情页 + +**基本信息**:名称、编码、状态、租户、描述、创建时间 + +**项目成员管理**: +- 成员列表(用户名、角色、添加时间) +- 添加成员:选择用户 + 角色(member/admin/viewer) +- 移除成员 + +**资源授权(ACL)**: +- 弹窗编辑器,逐条配置授权规则 +- 主体类型:用户(下拉选已有用户)或项目角色(member/admin/viewer) +- 权限:read / write / execute / download / delete / share(多选 checkbox) +- 增删行后统一保存 + +### 当前缺失 +- [ ] 编辑项目基本信息(名称、描述) +- [ ] 项目启停/归档操作(`archiveProject` API 已有,UI 缺失) +- [ ] 项目下的资源使用统计(模型数、数据集数、任务数) + +--- + +## 3. 审批管理 + +**对应页面**:`ApprovalTemplateView.vue`(模板)+ `ApprovalInstanceView.vue`(实例) +**后端接口**:模板 CRUD、实例列表+决策 + +### 审批模板 + +**展示列**:模板名、步骤数、创建时间 + +**新建模板**: +| 字段 | 控件 | 说明 | +|------|------|------| +| 模板名称 | 文本输入 | 必填 | +| 审批步骤 | 文本域 | **需手写 JSON 字符串**,体验差 | + +**当前缺失**: +- [ ] 可视化步骤编辑(拖拽添加步骤、选审批人) +- [ ] 模板编辑/删除(API 已有,UI 缺失) +- [ ] 模板详情页 + +### 审批中心 + +**展示列**:资源类型、资源ID、状态(Tag着色)、发起人、创建时间 + +**工具栏**:状态筛选(待审批/已通过/已拒绝)、资源类型/ID 搜索 + +**行操作**:通过/拒绝(弹窗填写审批意见) + +**当前缺失**: +- [ ] "我发起的"审批视角 +- [ ] 审批流转详情(谁审批了、什么时间) +- [ ] 撤回功能 + +--- + +## 4. 审计日志 + +**对应页面**:`AuditLogView.vue` +**后端接口**:`GET /system/audit-logs`(多条件筛选+分页)、`GET /system/audit-logs/export`(CSV导出) + +### 列表页 + +**展示列**:时间、租户ID、项目ID、操作人ID、动作、目标类型、目标ID、详情、IP + +**筛选条件**:租户ID、项目ID、操作人ID、动作、目标类型、开始时间、结束时间 + +**工具栏**:CSV 导出按钮(最多 10000 条) + +### 当前缺失 +- [ ] 分页控件(数据量大时需要翻页) +- [ ] 筛选字段改为下拉选择(当前全是文本输入,不知道有哪些可选值) +- [ ] 详情弹窗(点击某条记录查看完整信息) + +--- + +## 5. 用户设置 + +**对应页面**:`UserSettingsView.vue`(列表)+ `UserCreateView.vue`(创建)+ `UserPermissionView.vue`(权限弹窗) +**后端接口**:`GET/POST/PUT/DELETE /users`、`POST /users/:id/reset-password` + +### 列表页 + +**展示列**:账号、显示名、角色、状态、页面权限、创建时间 + +**行操作**: +| 操作 | 说明 | +|------|------| +| 启停开关 | protected 用户和自己不可操作 | +| 重置密码 | 弹窗确认,默认密码 `Platform@123` | +| 页面权限 | 弹窗 checkbox 组,12 个模块可选:看板/模型训练/模型评测/模型推理/模型管理/数据集/数据处理/数据转换/算力/平台性能/查看日志/用户设置 | +| 删除 | 确认弹窗,protected 用户和自己不可操作 | + +### 创建页 + +**表单字段**:账号、显示名、初始密码、角色(超级管理员/操作员/观察员)、状态、页面权限 + +### 当前缺失 +- [ ] 用户编辑页面(修改显示名、角色等,`updateUser` API 已有,UI 缺失) +- [ ] 权限码显示为英文(如 `fine-tune`),无中文翻译 + +--- + +## 6. 平台性能 + +**对应页面**:`HardwareView.vue` +**后端接口**:`GET /system-info`、`GET /health`(顶部栏实时指标) + +### 页面内容 + +**概览卡片**:CPU(型号/核心数/使用率)、内存(已用/总量/使用率)、磁盘(已用/总量/使用率)、网络吞吐 + +**趋势图**:CPU/内存/磁盘/GPU 利用率折线图(最近 60 次采样,1/3/5/10秒自动刷新) + +**GPU 资源池**:每张卡展示名称、状态、利用率、显存、温度、功耗、风扇转速;点击卡片查看详情抽屉(设备属性、趋势图、进程列表) + +**主机信息**:OS、运行时长、进程数、GPU 驱动版本 + +### 当前缺失 +- [ ] 历史数据持久化(刷新后采样清空) +- [ ] 告警阈值设置 +- [ ] 网络吞吐数据显示为空 + +--- + +## 7. 查看日志 + +**对应页面**:`LogsView.vue`(系统日志 + 训练日志双 Tab) + +### 系统日志 + +- 按日期选择日志文件 +- 关键词搜索 +- 日志级别筛选(INFO/WARN/ERROR/DEBUG) +- 自动刷新(5/10/30/60 秒可调) + +### 训练日志 + +- 按 PID 选择训练日志文件 +- 同样支持搜索和级别筛选 + +### 当前缺失 +- [ ] 日志下载/导出 +- [ ] 分页或虚拟滚动(大文件加载慢) +- [ ] 行号显示 + +--- + +## 8. 留存策略 ⚠️ 前端完全缺失 + +**后端 API 已完整实现**(CRUD 5 个端点),**前端 `retention.ts` 模块已封装**,但: +- 侧边栏无菜单入口 +- 路由未配置 +- 无任何 Vue 页面 + +--- + +## 数据流转关系 + +``` +用户登录 → 分配角色(admin/operator/viewer) + 页面权限 + │ + ├─ 创建租户(配额:GPU/存储/项目数) + │ └─ 关联用户 + │ + ├─ 创建项目(关联租户,选择编码ID) + │ ├─ 项目成员(角色:member/admin/viewer) + │ └─ 资源授权(ACL):谁对什么资源有什么权限 + │ + ├─ 创建审批模板(定义审批流程) + │ └─ 审批实例:敏感操作需要审批(通过/拒绝) + │ + ├─ 审计日志:所有操作自动记录(谁在什么时间做了什么) + │ + └─ 留存策略:定义数据保留周期,自动清理过期数据 +``` + +--- + +## 完成度总表 + +| 模块 | 列表 | 新建 | 编辑 | 删除 | 搜索 | 筛选 | 导出 | 完成度 | +|------|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:| +| 租户管理 | ✅ | ✅ | 仅配额 | ✅ | ❌ | ❌ | ❌ | 75% | +| 项目空间 | ✅ | ✅ | ❌ | ✅ | ✅ | 按租户 | ❌ | 80% | +| 审批模板 | ✅ | ✅(JSON) | ❌ | ❌ | ❌ | ❌ | ❌ | 45% | +| 审批中心 | ✅ | N/A | N/A | ❌ | ✅ | 按状态 | ❌ | 65% | +| 审计日志 | ✅ | N/A | N/A | N/A | ❌ | 多字段 | CSV | 70% | +| 用户设置 | ✅ | ✅ | 仅权限 | ✅ | ❌ | ❌ | ❌ | 75% | +| 平台性能 | ✅ | N/A | N/A | N/A | N/A | N/A | ❌ | 80% | +| 查看日志 | ✅ | N/A | N/A | N/A | 关键词 | 按级别 | ❌ | 70% | +| **留存策略** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | **0%** |