From c2fe3585d55661f49bd5ae02dce0a901c744e241 Mon Sep 17 00:00:00 2001 From: iHeyTang Date: Tue, 19 Aug 2025 15:18:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=9D=99=E6=80=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=94=AF=E6=8C=81=E5=92=8C=E4=BB=AA=E8=A1=A8?= =?UTF-8?q?=E6=9D=BF=E8=B7=AF=E7=94=B1=EF=BC=8C=E7=A7=BB=E9=99=A4=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=E7=9B=B8=E5=85=B3=E8=B7=AF=E7=94=B1=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=E5=92=8C=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 3 +- workflow_service/main.py | 21 +- workflow_service/routes/monitor.py | 498 ------------------------ workflow_service/static/css/monitor.css | 302 ++++++++++++++ workflow_service/static/index.html | 49 +++ workflow_service/static/js/monitor.js | 134 +++++++ 6 files changed, 504 insertions(+), 503 deletions(-) delete mode 100644 workflow_service/routes/monitor.py create mode 100644 workflow_service/static/css/monitor.css create mode 100644 workflow_service/static/index.html create mode 100644 workflow_service/static/js/monitor.js diff --git a/requirements.txt b/requirements.txt index 0b894e4..bf8816f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ boto3 aiosqlite sqlalchemy greenlet -psutil \ No newline at end of file +psutil +jinja2 \ No newline at end of file diff --git a/workflow_service/main.py b/workflow_service/main.py index a1e6f2d..b033705 100644 --- a/workflow_service/main.py +++ b/workflow_service/main.py @@ -1,13 +1,16 @@ import os +from pathlib import Path +from fastapi.responses import FileResponse import uvicorn from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles from workflow_service.comfy.comfy_server import server_manager from workflow_service.database.api import init_db from workflow_service.config import Settings -from workflow_service.routes import service, workflow, run, runx, monitor, comfy_server +from workflow_service.routes import service, workflow, run, runx, comfy_server from workflow_service.comfy.comfy_queue import queue_manager settings = Settings() @@ -23,14 +26,24 @@ web_app.add_middleware( allow_headers=["*"], # 允许所有请求头 ) +# 挂载静态文件目录 +static_dir = Path(__file__).parent / "static" +if static_dir.exists(): + web_app.mount("/static", StaticFiles(directory=str(static_dir)), name="static") + web_app.include_router(service.service_router) web_app.include_router(workflow.workflow_router) web_app.include_router(run.run_router) web_app.include_router(runx.runx_router) -web_app.include_router(monitor.monitor_router) web_app.include_router(comfy_server.router) +@web_app.get("/") +async def dashboard(): + """监控仪表板页面""" + return FileResponse(str(Path(__file__).parent / "static" / "index.html")) + + @web_app.on_event("startup") async def startup_event(): """服务启动时,初始化数据库。""" @@ -40,11 +53,11 @@ async def startup_event(): print(f"检测到 {len(servers)} 个 ComfyUI 服务器配置。") for server in servers: print(f" - 服务器: {server.http_url}") - + # 启动队列监控任务 await queue_manager.start_queue_monitor() print("队列监控任务已启动") - + except ValueError as e: print(f"错误: 无法在启动时获取服务器信息: {e}") diff --git a/workflow_service/routes/monitor.py b/workflow_service/routes/monitor.py deleted file mode 100644 index 80f5e9e..0000000 --- a/workflow_service/routes/monitor.py +++ /dev/null @@ -1,498 +0,0 @@ -from fastapi import APIRouter -from fastapi.responses import HTMLResponse - -monitor_router = APIRouter(prefix="/monitor", tags=["监控"]) - -@monitor_router.get("/", response_class=HTMLResponse) -async def monitor_dashboard(): - """监控仪表板页面""" - html_content = """ - - - - - - ComfyUI 工作流监控 - - - -
- - -
-
-
--
-
运行中任务
-
-
-
--
-
等待中任务
-
-
-
--
-
已完成任务
-
-
-
--
-
24小时总任务
-
-
- -
-

🖥️ 服务器状态

-
- -
-
- -
-

📋 最近任务

-
- -
-
-
- - - - - """ - return HTMLResponse(content=html_content) diff --git a/workflow_service/static/css/monitor.css b/workflow_service/static/css/monitor.css new file mode 100644 index 0000000..b9814d1 --- /dev/null +++ b/workflow_service/static/css/monitor.css @@ -0,0 +1,302 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + min-height: 100vh; + padding: 24px; + color: #2c3e50; + line-height: 1.6; +} + +.container { + max-width: 1200px; + margin: 0 auto; +} + +.refresh-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + margin-bottom: 24px; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); + transition: all 0.3s ease; +} + +.refresh-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-bottom: 24px; +} + +.stat-card { + background: rgba(255, 255, 255, 0.95); + padding: 24px; + border-radius: 16px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + text-align: center; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + transition: all 0.3s ease; +} + +.stat-card:hover { + transform: translateY(-4px); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); +} + +.stat-number { + font-size: 2.2em; + font-weight: 700; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin: 8px 0; +} + +.stat-label { + color: #7f8c8d; + font-size: 0.9em; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.section { + background: rgba(255, 255, 255, 0.95); + padding: 24px; + border-radius: 16px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + margin-bottom: 24px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.section h3 { + margin-bottom: 20px; + color: #2c3e50; + font-size: 1.3em; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; +} + +.server-status { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; +} + +.server-card { + border: 1px solid #e1e8ed; + border-radius: 8px; + padding: 12px; + background: #f8f9fa; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.server-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: #e1e8ed; +} + +.server-card.online { + border-color: #27ae60; + background: #f8fff9; +} + +.server-card.online::before { + background: #27ae60; +} + +.server-card.offline { + border-color: #e74c3c; + background: #fff8f8; +} + +.server-card.offline::before { + background: #e74c3c; +} + +.server-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} + +.server-card h4 { + color: #2c3e50; + margin: 0; + font-size: 1em; + font-weight: 600; + white-space: nowrap; +} + +.server-url { + color: #3498db; + font-size: 0.8em; + font-weight: 500; + font-family: monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; +} + +.server-info { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + font-size: 0.8em; + margin-top: 8px; +} + +.info-item { + display: flex; + align-items: center; + gap: 4px; + white-space: nowrap; +} + +.info-separator { + color: #e1e8ed; + margin: 0 4px; +} + +.status-icon { + font-size: 0.9em; +} + +.status-text { + font-weight: 600; + color: #2c3e50; +} + +.info-label { + color: #7f8c8d; + font-weight: 500; + font-size: 0.75em; +} + +.info-value { + color: #2c3e50; + font-weight: 600; + font-size: 0.75em; +} + +.info-value:has(span[style*="color: #e74c3c"]) { + color: #e74c3c; +} + +.info-value:has(span[style*="color: #f39c12"]) { + color: #f39c12; +} + +.info-value:has(span[style*="color: #27ae60"]) { + color: #27ae60; +} + +.status-indicator { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 8px; +} + +.status-online { background-color: #27ae60; } +.status-offline { background-color: #e74c3c; } + +.task-list { + max-height: 500px; + overflow-y: auto; +} + +.task-item { + border: 1px solid #e1e8ed; + border-radius: 8px; + padding: 16px; + margin: 8px 0; + background: #f8f9fa; + transition: all 0.3s ease; +} + +.task-item:hover { + background: #ffffff; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.task-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.task-name { + font-weight: 600; + color: #2c3e50; +} + +.task-status { + padding: 4px 12px; + border-radius: 20px; + font-size: 0.8em; + font-weight: 600; + color: white; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.status-pending { background: #f39c12; } +.status-running { background: #3498db; } +.status-completed { background: #27ae60; } +.status-failed { background: #e74c3c; } + +.task-meta { + color: #7f8c8d; + font-size: 0.85em; +} + +.error-message { + color: #e74c3c; + font-size: 0.85em; + margin-top: 8px; + padding: 8px; + background: #fff5f5; + border-radius: 6px; + border-left: 3px solid #e74c3c; +} + +@media (max-width: 768px) { + body { padding: 16px; } + .stats-grid { grid-template-columns: repeat(2, 1fr); } + .server-status { grid-template-columns: 1fr; } + .server-info { grid-template-columns: 1fr; } +} diff --git a/workflow_service/static/index.html b/workflow_service/static/index.html new file mode 100644 index 0000000..6624a2e --- /dev/null +++ b/workflow_service/static/index.html @@ -0,0 +1,49 @@ + + + + + + ComfyUI 工作流监控 + + + +
+ + +
+
+
--
+
运行中任务
+
+
+
--
+
等待中任务
+
+
+
--
+
已完成任务
+
+
+
--
+
24小时总任务
+
+
+ +
+

🖥️ 服务器状态

+
+ +
+
+ +
+

📋 最近任务

+
+ +
+
+
+ + + + diff --git a/workflow_service/static/js/monitor.js b/workflow_service/static/js/monitor.js new file mode 100644 index 0000000..2150047 --- /dev/null +++ b/workflow_service/static/js/monitor.js @@ -0,0 +1,134 @@ +async function refreshData() { + try { + // 获取运行概览 - 更新为新的API路径 + const runMetrics = await fetch('/api/run/metrics').then(r => r.json()); + document.getElementById('running-tasks').textContent = runMetrics.running_tasks; + document.getElementById('pending-tasks').textContent = runMetrics.pending_tasks; + document.getElementById('completed-tasks').textContent = runMetrics.completed_tasks; + document.getElementById('total-tasks').textContent = runMetrics.total_tasks_24h; + + // 获取服务器状态 - 使用现有的list接口 + const serverStatus = await fetch('/api/comfy/list').then(r => r.json()); + updateServerStatus(serverStatus); + + // 获取运行列表 - 更新为新的API路径 + const runs = await fetch('/api/run').then(r => r.json()); + updateRecentTasks(runs); + + } catch (error) { + console.error('刷新数据失败:', error); + } +} + +function updateServerStatus(servers) { + const container = document.getElementById('server-status'); + container.innerHTML = ''; + + if (servers.length === 0) { + container.innerHTML = '

暂无服务器信息

'; + return; + } + + servers.forEach(server => { + const statusClass = server.status === 'online' ? 'online' : 'offline'; + const statusColor = server.status === 'online' ? 'status-online' : 'status-offline'; + + // 格式化时间 + const formatTime = (timeStr) => { + if (!timeStr) return 'N/A'; + try { + const date = new Date(timeStr); + const now = new Date(); + const diff = now - date; + + if (diff < 60000) return '刚刚'; + if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'; + if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'; + return date.toLocaleDateString(); + } catch { + return timeStr; + } + }; + + // 格式化健康检查时间 + const formatHealthCheck = (timeStr) => { + if (!timeStr) return 'N/A'; + try { + const date = new Date(timeStr); + const now = new Date(); + const diff = now - date; + + if (diff > 86400000) return '⚠️ 检查超时'; // 超过1天 + if (diff > 3600000) return '⚠️ 检查延迟'; // 超过1小时 + if (diff < 60000) return '✅ 刚刚'; + if (diff < 3600000) return `✅ ${Math.floor(diff / 60000)}分钟前`; + return `✅ ${Math.floor(diff / 3600000)}小时前`; + } catch { + return timeStr; + } + }; + + container.innerHTML += ` +
+
+

${server.name || '未命名'}

+ 📍 ${server.http_url} +
+
+
+ ${server.status === 'online' ? '🟢' : '🔴'} + ${server.status === 'online' ? '在线' : '离线'} +
+ | +
+ 任务: ${server.current_tasks || 0}/${server.max_concurrent_tasks || 1} +
+ | +
+ 检查: ${formatHealthCheck(server.last_health_check)} +
+
+ ${server.error ? `
${server.error}
` : ''} +
+ `; + }); +} + +function updateRecentTasks(tasks) { + const container = document.getElementById('recent-tasks'); + container.innerHTML = ''; + + if (tasks.length === 0) { + container.innerHTML = '

暂无任务记录

'; + return; + } + + tasks.forEach(task => { + const statusClass = 'status-' + task.status.toLowerCase(); + const statusText = { + 'pending': '等待中', + 'running': '运行中', + 'completed': '已完成', + 'failed': '失败' + }[task.status] || task.status; + + container.innerHTML += ` +
+
+
${task.workflow_name || '未命名工作流'}
+ ${statusText} +
+
+
ID: ${task.id}
+
创建时间: ${new Date(task.created_at).toLocaleString()}
+
+
+ `; + }); +} + +// 页面加载时自动刷新数据 +document.addEventListener('DOMContentLoaded', refreshData); + +// 每30秒自动刷新一次 +setInterval(refreshData, 30000);