From d5d7e45dedd5334071ecce77efbd0301c56acc94 Mon Sep 17 00:00:00 2001 From: cnbugs Date: Sat, 25 Jul 2026 10:32:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20K8S=E6=99=BA=E8=83=BD=E8=AF=8A=E6=96=AD?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=20-=20=E4=B8=80=E9=94=AE=E8=AF=8A=E6=96=AD+A?= =?UTF-8?q?I=E5=88=86=E6=9E=90+Web=E4=BB=AA=E8=A1=A8=E7=9B=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端: FastAPI + kubectl 9大检查项(Pod/Node/Event/Deploy/Service/PVC/资源/网络) - AI Agent: OpenAI兼容接口自动分析故障根因+多轮追问对话 - 前端: Vue3 + Element Plus 仪表盘, 健康评分, 问题列表, 资源监控 - 一键启动脚本 start.sh --- .gitignore | 24 + README.md | 83 +++ backend/__init__.py | 0 backend/ai_agent.py | 80 +++ backend/config.py | 11 + backend/diagnosis.py | 523 +++++++++++++++ backend/main.py | 108 +++ frontend/index.html | 12 + frontend/package-lock.json | 1286 ++++++++++++++++++++++++++++++++++++ frontend/package.json | 10 + frontend/src/App.vue | 366 ++++++++++ frontend/src/main.js | 13 + frontend/vite.config.js | 18 + start.sh | 40 ++ 14 files changed, 2574 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/__init__.py create mode 100644 backend/ai_agent.py create mode 100644 backend/config.py create mode 100644 backend/diagnosis.py create mode 100644 backend/main.py create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/main.js create mode 100644 frontend/vite.config.js create mode 100755 start.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a3d4a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Dependencies +node_modules/ +frontend/node_modules/ + +# Build output +frontend/dist/ + +# Python +__pycache__/ +*.pyc +*.pyo +venv/ +.venv/ + +# Environment +.env + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..54a5079 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# K8S 智能诊断平台 + +一键诊断 Kubernetes 集群健康状态,接入 AI Agent 自动分析故障根因并给出修复建议。 + +![界面预览](/home/cnbugs/.hermes/cache/screenshots/browser_screenshot_b258ca152c1e489fa186583946ecbd9e.png) + +## 功能特性 + +- **一键诊断**:9 大检查项全面扫描集群状态 + - 集群信息、Pod 状态、Node 状态、异常事件 + - Deployment 副本检查、Service/Endpoint 检查 + - PVC 存储检查、节点资源使用率、网络策略 +- **健康评分**:100 分制量化集群健康度(严重问题 -15 分,警告 -5 分) +- **AI 智能分析**:接入 OpenAI 兼容接口,自动分析诊断报告 + - 根因定位、影响评估、修复命令、优先级排序、预防建议 +- **AI 追问对话**:基于诊断上下文多轮追问,深入排查问题 +- **Web 仪表盘**:Vue3 + Element Plus,实时展示诊断结果 + +## 快速开始 + +```bash +# 1. 确保 kubectl 已配置 +kubectl cluster-info + +# 2. 启动服务 +./start.sh + +# 3. 打开浏览器 +# http://localhost:8900 +``` + +## 配置 + +编辑 `.env` 文件: + +```env +# AI 分析接口 (OpenAI 兼容格式) +AI_API_BASE=http://localhost:8648/v1 +AI_API_KEY=your-api-key +AI_MODEL=qwen3.8-max-preview + +# 服务端口 +PORT=8900 + +# Kubeconfig 路径 (留空使用默认 ~/.kube/config) +KUBECONFIG_PATH= +``` + +## API 接口 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/health` | 健康检查 | +| GET | `/api/checks` | 列出所有诊断项 | +| POST | `/api/diagnose` | 执行诊断 `{"namespace":"", "checks":[]}` | +| GET | `/api/diagnosis/latest` | 获取最近诊断结果 | +| POST | `/api/analyze` | AI 分析诊断结果 `{"question":""}` | +| POST | `/api/chat` | AI 多轮对话 `{"messages":[...]}` | +| GET | `/api/report` | 获取诊断报告文本 | + +## 技术栈 + +- **后端**:Python 3 + FastAPI + kubernetes-client (kubectl) +- **前端**:Vue 3 + Element Plus + Vite + marked +- **AI**:OpenAI 兼容 Chat Completions API + +## 项目结构 + +``` +k8smanager-cli/ +├── backend/ +│ ├── main.py # FastAPI 主应用 +│ ├── config.py # 配置管理 +│ ├── diagnosis.py # K8S 诊断引擎 (9 大检查项) +│ └── ai_agent.py # AI Agent 分析模块 +├── frontend/ +│ ├── src/App.vue # 仪表盘界面 +│ ├── index.html +│ └── vite.config.js +├── .env # 配置文件 +├── start.sh # 一键启动脚本 +└── README.md +``` diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/ai_agent.py b/backend/ai_agent.py new file mode 100644 index 0000000..639fdfb --- /dev/null +++ b/backend/ai_agent.py @@ -0,0 +1,80 @@ +"""AI Agent 分析模块 - 调用 OpenAI 兼容接口分析诊断结果""" +import httpx +from backend.config import AI_API_BASE, AI_API_KEY, AI_MODEL + +SYSTEM_PROMPT = """你是一位资深的 Kubernetes 运维专家和 SRE 工程师。你的任务是分析 K8S 集群诊断报告,找出根本原因并给出修复建议。 + +分析要求: +1. **问题定位**:明确指出每个问题的根本原因(Root Cause),不要只描述表面现象 +2. **影响评估**:说明每个问题对业务的潜在影响 +3. **修复方案**:给出具体的修复命令或操作步骤(kubectl 命令、YAML 修改等) +4. **优先级排序**:按紧急程度排序,先处理影响最大的问题 +5. **预防建议**:给出避免类似问题再次发生的建议 + +输出格式:使用清晰的 Markdown,包含标题、列表、代码块。语言使用中文。""" + + +async def analyze_with_ai(report: str, question: str = "") -> dict: + """调用 AI 分析诊断报告""" + user_msg = f"以下是 K8S 集群诊断报告,请进行详细分析:\n\n{report}" + if question: + user_msg += f"\n\n用户额外问题:{question}" + + try: + async with httpx.AsyncClient(timeout=120) as client: + resp = await client.post( + f"{AI_API_BASE}/chat/completions", + headers={ + "Authorization": f"Bearer {AI_API_KEY}", + "Content-Type": "application/json", + }, + json={ + "model": AI_MODEL, + "messages": [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": user_msg}, + ], + "temperature": 0.3, + "max_tokens": 4096, + }, + ) + resp.raise_for_status() + data = resp.json() + content = data["choices"][0]["message"]["content"] + return {"success": True, "analysis": content, "model": AI_MODEL} + except httpx.HTTPStatusError as e: + return {"success": False, "analysis": f"AI 接口返回错误: {e.response.status_code} - {e.response.text[:500]}", "model": AI_MODEL} + except httpx.ConnectError: + return {"success": False, "analysis": f"无法连接 AI 服务 ({AI_API_BASE}),请检查配置", "model": AI_MODEL} + except Exception as e: + return {"success": False, "analysis": f"AI 分析失败: {str(e)}", "model": AI_MODEL} + + +async def chat_with_ai(messages: list[dict], context: str = "") -> dict: + """多轮对话 - 基于诊断上下文追问""" + msgs = [{"role": "system", "content": SYSTEM_PROMPT}] + if context: + msgs.append({"role": "system", "content": f"当前诊断上下文:\n{context}"}) + msgs.extend(messages) + + try: + async with httpx.AsyncClient(timeout=120) as client: + resp = await client.post( + f"{AI_API_BASE}/chat/completions", + headers={ + "Authorization": f"Bearer {AI_API_KEY}", + "Content-Type": "application/json", + }, + json={ + "model": AI_MODEL, + "messages": msgs, + "temperature": 0.3, + "max_tokens": 4096, + }, + ) + resp.raise_for_status() + data = resp.json() + content = data["choices"][0]["message"]["content"] + return {"success": True, "reply": content} + except Exception as e: + return {"success": False, "reply": f"AI 对话失败: {str(e)}"} diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..055f292 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,11 @@ +"""配置管理""" +import os +from dotenv import load_dotenv + +load_dotenv() + +AI_API_BASE = os.getenv("AI_API_BASE", "http://localhost:8648/v1") +AI_API_KEY = os.getenv("AI_API_KEY", "hermes") +AI_MODEL = os.getenv("AI_MODEL", "qwen3.8-max-preview") +PORT = int(os.getenv("PORT", "8900")) +KUBECONFIG_PATH = os.getenv("KUBECONFIG_PATH", "") or None diff --git a/backend/diagnosis.py b/backend/diagnosis.py new file mode 100644 index 0000000..b5e9ed0 --- /dev/null +++ b/backend/diagnosis.py @@ -0,0 +1,523 @@ +"""K8S 集群诊断引擎 - 通过 kubectl 命令采集集群状态""" +import asyncio +import json +import shutil +from datetime import datetime +from typing import Optional + +KUBECTL = shutil.which("kubectl") or "/home/cnbugs/bin/kubectl" + + +async def _run_kubectl(args: list[str], timeout: int = 30) -> dict: + """执行 kubectl 命令,返回结构化结果""" + cmd = [KUBECTL] + args + ["-o", "json"] + try: + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) + if proc.returncode != 0: + return {"error": stderr.decode().strip(), "data": None} + return {"error": None, "data": json.loads(stdout.decode())} + except asyncio.TimeoutError: + return {"error": f"kubectl 命令超时 ({timeout}s)", "data": None} + except Exception as e: + return {"error": str(e), "data": None} + + +async def _run_kubectl_raw(args: list[str], timeout: int = 30) -> str: + """执行 kubectl 命令,返回原始文本""" + cmd = [KUBECTL] + args + try: + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) + return stdout.decode().strip() + except Exception: + return "" + + +def _parse_resource_quantity(q: str) -> float: + """解析 K8S 资源量 (如 500m, 2Gi) 为数值""" + if not q: + return 0 + q = str(q) + units = {"n": 1e-9, "u": 1e-6, "m": 1e-3, "": 1, "k": 1e3, "M": 1e6, "G": 1e9, "T": 1e12, + "Ki": 1024, "Mi": 1024**2, "Gi": 1024**3, "Ti": 1024**4} + for suffix, mult in sorted(units.items(), key=lambda x: -len(x[0])): + if suffix and q.endswith(suffix): + try: + return float(q[:-len(suffix)]) * mult + except ValueError: + return 0 + try: + return float(q) + except ValueError: + return 0 + + +# ─── 诊断项 ─────────────────────────────────────────────── + +async def check_cluster_info() -> dict: + """集群基本信息""" + ver = await _run_kubectl(["version"]) + nodes = await _run_kubectl(["get", "nodes"]) + namespaces = await _run_kubectl(["get", "namespaces"]) + return { + "name": "cluster_info", + "title": "集群信息", + "version": ver, + "nodes": nodes, + "namespaces": namespaces, + } + + +async def check_pods(namespace: str = "") -> dict: + """Pod 状态检查""" + args = ["get", "pods"] + if namespace: + args += ["-n", namespace] + else: + args += ["--all-namespaces"] + result = await _run_kubectl(args) + + issues = [] + pods_summary = {"total": 0, "running": 0, "pending": 0, "failed": 0, "unknown": 0, "crashloop": 0} + + if result["data"] and "items" in result["data"]: + for pod in result["data"]["items"]: + pods_summary["total"] += 1 + phase = pod.get("status", {}).get("phase", "Unknown") + name = pod["metadata"]["name"] + ns = pod["metadata"].get("namespace", "default") + + if phase == "Running": + pods_summary["running"] += 1 + # 检查重启次数 + for cs in pod.get("status", {}).get("containerStatuses", []): + restarts = cs.get("restartCount", 0) + if restarts > 5: + issues.append({ + "level": "warning", + "resource": f"{ns}/{name}", + "message": f"容器 {cs['name']} 重启 {restarts} 次", + }) + state = cs.get("state", {}) + if "waiting" in state: + reason = state["waiting"].get("reason", "") + if reason in ("CrashLoopBackOff", "ErrImagePull", "ImagePullBackOff", "CreateContainerConfigError"): + pods_summary["crashloop"] += 1 + issues.append({ + "level": "critical", + "resource": f"{ns}/{name}", + "message": f"容器 {cs['name']} 状态异常: {reason}", + }) + elif phase == "Pending": + pods_summary["pending"] += 1 + issues.append({"level": "warning", "resource": f"{ns}/{name}", "message": "Pod 处于 Pending 状态"}) + elif phase == "Failed": + pods_summary["failed"] += 1 + issues.append({"level": "critical", "resource": f"{ns}/{name}", "message": "Pod 处于 Failed 状态"}) + else: + pods_summary["unknown"] += 1 + + return { + "name": "pods", + "title": "Pod 状态检查", + "summary": pods_summary, + "issues": issues, + "raw": result, + } + + +async def check_nodes() -> dict: + """Node 状态检查""" + result = await _run_kubectl(["get", "nodes"]) + issues = [] + nodes_summary = {"total": 0, "ready": 0, "not_ready": 0} + + if result["data"] and "items" in result["data"]: + for node in result["data"]["items"]: + nodes_summary["total"] += 1 + name = node["metadata"]["name"] + conditions = node.get("status", {}).get("conditions", []) + is_ready = False + for cond in conditions: + if cond["type"] == "Ready" and cond["status"] == "True": + is_ready = True + if cond["status"] == "True" and cond["type"] in ("MemoryPressure", "DiskPressure", "PIDPressure"): + issues.append({ + "level": "critical", + "resource": name, + "message": f"节点存在 {cond['type']} 压力", + }) + if is_ready: + nodes_summary["ready"] += 1 + else: + nodes_summary["not_ready"] += 1 + issues.append({"level": "critical", "resource": name, "message": "节点 NotReady"}) + + # 资源使用 + alloc = node.get("status", {}).get("allocatable", {}) + cap = node.get("status", {}).get("capacity", {}) + if alloc and cap: + for res in ("cpu", "memory"): + a = _parse_resource_quantity(alloc.get(res, "0")) + c = _parse_resource_quantity(cap.get(res, "0")) + if c > 0 and a / c < 0.1: + issues.append({ + "level": "warning", + "resource": name, + "message": f"节点 {res} 可分配量不足 10%", + }) + + return { + "name": "nodes", + "title": "Node 状态检查", + "summary": nodes_summary, + "issues": issues, + "raw": result, + } + + +async def check_events(namespace: str = "") -> dict: + """异常事件检查""" + args = ["get", "events", "--field-selector", "type!=Normal", "--sort-by=.lastTimestamp"] + if namespace: + args += ["-n", namespace] + else: + args += ["--all-namespaces"] + result = await _run_kubectl(args) + + issues = [] + events_list = [] + if result["data"] and "items" in result["data"]: + for ev in result["data"]["items"][-50:]: # 最近50条 + events_list.append({ + "namespace": ev["metadata"].get("namespace", ""), + "reason": ev.get("reason", ""), + "message": ev.get("message", ""), + "type": ev.get("type", ""), + "object": f"{ev.get('involvedObject', {}).get('kind', '')}/{ev.get('involvedObject', {}).get('name', '')}", + "lastSeen": ev.get("lastTimestamp", ""), + "count": ev.get("count", 1), + }) + if ev.get("type") == "Warning": + issues.append({ + "level": "warning", + "resource": f"{ev['metadata'].get('namespace', '')}/{ev.get('involvedObject', {}).get('name', '')}", + "message": f"[{ev.get('reason', '')}] {ev.get('message', '')[:200]}", + }) + + return { + "name": "events", + "title": "异常事件检查", + "warning_count": len(issues), + "events": events_list, + "issues": issues, + "raw": result, + } + + +async def check_deployments(namespace: str = "") -> dict: + """Deployment 状态检查""" + args = ["get", "deployments"] + if namespace: + args += ["-n", namespace] + else: + args += ["--all-namespaces"] + result = await _run_kubectl(args) + + issues = [] + deploys = [] + if result["data"] and "items" in result["data"]: + for dep in result["data"]["items"]: + name = dep["metadata"]["name"] + ns = dep["metadata"].get("namespace", "default") + spec_replicas = dep.get("spec", {}).get("replicas", 0) + status = dep.get("status", {}) + ready = status.get("readyReplicas", 0) or 0 + available = status.get("availableReplicas", 0) or 0 + updated = status.get("updatedReplicas", 0) or 0 + + deploys.append({ + "namespace": ns, "name": name, + "desired": spec_replicas, "ready": ready, + "available": available, "updated": updated, + }) + if ready < spec_replicas: + issues.append({ + "level": "critical" if ready == 0 else "warning", + "resource": f"{ns}/{name}", + "message": f"副本不足: 期望 {spec_replicas}, 就绪 {ready}", + }) + if updated < spec_replicas: + issues.append({ + "level": "warning", + "resource": f"{ns}/{name}", + "message": f"滚动更新未完成: 已更新 {updated}/{spec_replicas}", + }) + + return { + "name": "deployments", + "title": "Deployment 状态检查", + "deployments": deploys, + "issues": issues, + "raw": result, + } + + +async def check_services(namespace: str = "") -> dict: + """Service / Endpoint 检查""" + args = ["get", "endpoints"] + if namespace: + args += ["-n", namespace] + else: + args += ["--all-namespaces"] + result = await _run_kubectl(args) + + issues = [] + if result["data"] and "items" in result["data"]: + for ep in result["data"]["items"]: + name = ep["metadata"]["name"] + ns = ep["metadata"].get("namespace", "default") + subsets = ep.get("subsets", []) + has_addr = any(s.get("addresses") for s in subsets) + if not has_addr and name != "kubernetes": + issues.append({ + "level": "warning", + "resource": f"{ns}/{name}", + "message": "Service 没有可用的后端 Endpoint", + }) + + return { + "name": "services", + "title": "Service/Endpoint 检查", + "issues": issues, + "raw": result, + } + + +async def check_pvc(namespace: str = "") -> dict: + """PVC 状态检查""" + args = ["get", "pvc"] + if namespace: + args += ["-n", namespace] + else: + args += ["--all-namespaces"] + result = await _run_kubectl(args) + + issues = [] + if result["data"] and "items" in result["data"]: + for pvc in result["data"]["items"]: + name = pvc["metadata"]["name"] + ns = pvc["metadata"].get("namespace", "default") + phase = pvc.get("status", {}).get("phase", "") + if phase != "Bound": + issues.append({ + "level": "critical", + "resource": f"{ns}/{name}", + "message": f"PVC 状态异常: {phase}", + }) + + return { + "name": "pvc", + "title": "PVC 存储检查", + "issues": issues, + "raw": result, + } + + +async def check_resource_usage() -> dict: + """节点资源使用率 (需要 metrics-server)""" + raw = await _run_kubectl_raw(["top", "nodes", "--no-headers"]) + issues = [] + nodes_usage = [] + + if raw and "error" not in raw.lower(): + for line in raw.strip().split("\n"): + parts = line.split() + if len(parts) >= 5: + name = parts[0] + cpu_pct = parts[1].rstrip("%") + mem_pct = parts[3].rstrip("%") + try: + cpu_v, mem_v = float(cpu_pct), float(mem_pct) + nodes_usage.append({"name": name, "cpu_pct": cpu_v, "mem_pct": mem_v}) + if cpu_v > 90: + issues.append({"level": "critical", "resource": name, "message": f"CPU 使用率 {cpu_v}%"}) + elif cpu_v > 75: + issues.append({"level": "warning", "resource": name, "message": f"CPU 使用率 {cpu_v}%"}) + if mem_v > 90: + issues.append({"level": "critical", "resource": name, "message": f"内存使用率 {mem_v}%"}) + elif mem_v > 75: + issues.append({"level": "warning", "resource": name, "message": f"内存使用率 {mem_v}%"}) + except ValueError: + pass + else: + return { + "name": "resource_usage", + "title": "资源使用率", + "issues": [], + "nodes": [], + "note": "metrics-server 不可用,跳过资源使用率检查", + "raw": {"error": raw}, + } + + return { + "name": "resource_usage", + "title": "资源使用率", + "issues": issues, + "nodes": nodes_usage, + "raw": {"text": raw}, + } + + +async def check_network_policies(namespace: str = "") -> dict: + """网络策略检查""" + args = ["get", "networkpolicies"] + if namespace: + args += ["-n", namespace] + else: + args += ["--all-namespaces"] + result = await _run_kubectl(args) + count = len(result["data"].get("items", [])) if result["data"] else 0 + return { + "name": "network", + "title": "网络策略检查", + "policy_count": count, + "issues": [], + "raw": result, + } + + +# ─── 主诊断入口 ─────────────────────────────────────────── + +ALL_CHECKS = { + "cluster_info": check_cluster_info, + "pods": check_pods, + "nodes": check_nodes, + "events": check_events, + "deployments": check_deployments, + "services": check_services, + "pvc": check_pvc, + "resource_usage": check_resource_usage, + "network": check_network_policies, +} + + +async def run_full_diagnosis(namespace: str = "", checks: Optional[list[str]] = None) -> dict: + """执行完整诊断""" + start = datetime.now() + + selected = checks or list(ALL_CHECKS.keys()) + tasks = {} + for name in selected: + fn = ALL_CHECKS.get(name) + if fn: + if name in ("pods", "events", "deployments", "services", "pvc", "network"): + tasks[name] = fn(namespace) + else: + tasks[name] = fn() + + results = {} + for name, coro in tasks.items(): + try: + results[name] = await coro + except Exception as e: + results[name] = {"name": name, "title": name, "error": str(e), "issues": []} + + # 汇总 + all_issues = [] + for r in results.values(): + for issue in r.get("issues", []): + issue["check"] = r.get("title", r.get("name", "")) + all_issues.append(issue) + + critical = [i for i in all_issues if i["level"] == "critical"] + warning = [i for i in all_issues if i["level"] == "warning"] + + elapsed = (datetime.now() - start).total_seconds() + + # 健康评分 (100 分制) + score = 100 + score -= len(critical) * 15 + score -= len(warning) * 5 + score = max(0, score) + + if score >= 90: + status = "healthy" + elif score >= 60: + status = "warning" + else: + status = "critical" + + return { + "timestamp": start.isoformat(), + "elapsed_seconds": round(elapsed, 2), + "namespace": namespace or "all", + "score": score, + "status": status, + "critical_count": len(critical), + "warning_count": len(warning), + "issues": all_issues, + "checks": results, + } + + +def build_diagnosis_report(diagnosis: dict) -> str: + """将诊断结果构建为 AI 可读的文本报告""" + lines = [] + lines.append(f"# K8S 集群诊断报告") + lines.append(f"时间: {diagnosis['timestamp']}") + lines.append(f"命名空间: {diagnosis['namespace']}") + lines.append(f"健康评分: {diagnosis['score']}/100 ({diagnosis['status']})") + lines.append(f"严重问题: {diagnosis['critical_count']} 个, 警告: {diagnosis['warning_count']} 个") + lines.append("") + + if diagnosis["issues"]: + lines.append("## 发现的问题") + for i, issue in enumerate(diagnosis["issues"], 1): + icon = "🔴" if issue["level"] == "critical" else "🟡" + lines.append(f"{i}. {icon} [{issue['check']}] {issue['resource']}: {issue['message']}") + lines.append("") + + # Pod 摘要 + pods = diagnosis["checks"].get("pods", {}) + if pods.get("summary"): + s = pods["summary"] + lines.append(f"## Pod 状态: 总计 {s['total']}, Running {s['running']}, Pending {s['pending']}, Failed {s['failed']}, CrashLoop {s.get('crashloop', 0)}") + + # Node 摘要 + nodes = diagnosis["checks"].get("nodes", {}) + if nodes.get("summary"): + s = nodes["summary"] + lines.append(f"## Node 状态: 总计 {s['total']}, Ready {s['ready']}, NotReady {s['not_ready']}") + + # Deployment 摘要 + deps = diagnosis["checks"].get("deployments", {}) + if deps.get("deployments"): + lines.append("## Deployment 状态:") + for d in deps["deployments"]: + lines.append(f" - {d['namespace']}/{d['name']}: {d['ready']}/{d['desired']} ready") + + # 异常事件 + events = diagnosis["checks"].get("events", {}) + if events.get("events"): + lines.append(f"\n## 最近异常事件 (共 {len(events['events'])} 条):") + for ev in events["events"][-20:]: + lines.append(f" - [{ev['type']}] {ev['object']}: {ev['reason']} - {ev['message'][:150]}") + + # 资源使用 + usage = diagnosis["checks"].get("resource_usage", {}) + if usage.get("nodes"): + lines.append("\n## 节点资源使用:") + for n in usage["nodes"]: + lines.append(f" - {n['name']}: CPU {n['cpu_pct']}%, 内存 {n['mem_pct']}%") + + return "\n".join(lines) diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..4f6c64a --- /dev/null +++ b/backend/main.py @@ -0,0 +1,108 @@ +"""K8S 智能诊断平台 - FastAPI 主应用""" +import os +from datetime import datetime +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel +from typing import Optional + +from backend.config import PORT +from backend.diagnosis import run_full_diagnosis, build_diagnosis_report, ALL_CHECKS +from backend.ai_agent import analyze_with_ai, chat_with_ai + +app = FastAPI(title="K8S 智能诊断平台", version="1.0.0") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +# 存储最近诊断结果 (内存) +_last_diagnosis: Optional[dict] = None +_last_report: str = "" + + +class DiagnoseRequest(BaseModel): + namespace: str = "" + checks: Optional[list[str]] = None + + +class AnalyzeRequest(BaseModel): + question: str = "" + + +class ChatRequest(BaseModel): + messages: list[dict] + + +@app.get("/api/health") +async def health(): + return {"status": "ok", "time": datetime.now().isoformat()} + + +@app.get("/api/checks") +async def list_checks(): + """列出所有可用诊断项""" + return { + "checks": [ + {"name": name, "title": fn.__doc__ or name} + for name, fn in ALL_CHECKS.items() + ] + } + + +@app.post("/api/diagnose") +async def diagnose(req: DiagnoseRequest): + """执行一键诊断""" + global _last_diagnosis, _last_report + _last_diagnosis = await run_full_diagnosis(namespace=req.namespace, checks=req.checks) + _last_report = build_diagnosis_report(_last_diagnosis) + return _last_diagnosis + + +@app.get("/api/diagnosis/latest") +async def get_latest(): + """获取最近一次诊断结果""" + if _last_diagnosis is None: + raise HTTPException(status_code=404, detail="暂无诊断记录,请先执行诊断") + return _last_diagnosis + + +@app.post("/api/analyze") +async def analyze(req: AnalyzeRequest): + """AI 分析诊断结果""" + global _last_diagnosis, _last_report + if _last_diagnosis is None: + _last_diagnosis = await run_full_diagnosis() + _last_report = build_diagnosis_report(_last_diagnosis) + result = await analyze_with_ai(_last_report, question=req.question) + return result + + +@app.post("/api/chat") +async def chat(req: ChatRequest): + """AI 多轮对话 (基于诊断上下文追问)""" + result = await chat_with_ai(req.messages, context=_last_report) + return result + + +@app.get("/api/report") +async def get_report(): + """获取诊断报告文本""" + if not _last_report: + raise HTTPException(status_code=404, detail="暂无诊断报告") + return {"report": _last_report} + + +# 静态文件服务 (前端) +static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend", "dist") +if os.path.isdir(static_dir): + app.mount("/", StaticFiles(directory=static_dir, html=True), name="static") + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=PORT) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..d6f7ba1 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + K8S 智能诊断平台 + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..cd054e2 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1286 @@ +{ + "name": "frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "@vitejs/plugin-vue": "^6.0.8", + "element-plus": "^2.14.3", + "marked": "^18.0.7", + "vite": "^8.1.5", + "vue": "^3.5.40" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.40", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz", + "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz", + "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", + "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz", + "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/element-plus": { + "version": "2.14.3", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.14.3.tgz", + "integrity": "sha512-pJcvxcpZjYruNzuJhAeVwnbYjfNgzBKnWHwSVEhwzM2/kcLI3brzmtIBxtPqd4hQWJfD1PRnjoc1WipLw2eBGg==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.7.6", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.3.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.5" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "18.0.7", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.7.tgz", + "integrity": "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", + "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-sfc": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/server-renderer": "3.5.40", + "@vue/shared": "3.5.40" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.8.tgz", + "integrity": "sha512-troqCMmQodQDqUqn63NQaFi+CDSclSe7sc8VEBFqf5GFLqmGR2Ph3P2WEC7qwpRVyEWsTi/aAr4vyOe/B1hU3g==", + "license": "MIT" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..9554c9f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,10 @@ +{ + "name": "k8s-diagnosis-frontend", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..1fd4d5e --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,366 @@ + + + + + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..4aa35be --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,13 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import App from './App.vue' + +const app = createApp(App) +app.use(ElementPlus, { locale: zhCn }) +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} +app.mount('#app') diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..ed90458 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + port: 3001, + proxy: { + '/api': { + target: 'http://localhost:8900', + changeOrigin: true, + }, + }, + }, + build: { + outDir: 'dist', + }, +}) diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..5cde9c6 --- /dev/null +++ b/start.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# K8S 智能诊断平台 - 一键启动脚本 +set -e + +cd "$(dirname "$0")" + +PORT=${PORT:-8900} + +echo "=========================================" +echo " K8S 智能诊断平台" +echo " 端口: $PORT" +echo "=========================================" + +# 检查 kubectl +KUBECTL=$(which kubectl 2>/dev/null || echo "$HOME/bin/kubectl") +if [ ! -x "$KUBECTL" ]; then + echo "⚠️ 未找到 kubectl,请先安装" + exit 1 +fi +echo "✅ kubectl: $KUBECTL" + +# 检查 kubeconfig +if [ -f "$HOME/.kube/config" ]; then + echo "✅ kubeconfig: ~/.kube/config" +else + echo "⚠️ 未找到 ~/.kube/config,诊断将返回空结果" + echo " 请将 kubeconfig 放到 ~/.kube/config" +fi + +# 检查前端构建 +if [ ! -d "frontend/dist" ]; then + echo "📦 构建前端..." + cd frontend && npx vite build && cd .. +fi + +echo "" +echo "🚀 启动服务: http://localhost:$PORT" +echo "" + +PORT=$PORT python3 -m uvicorn backend.main:app --host 0.0.0.0 --port "$PORT"