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 工作流监控
-
-
-
-
-
-
-
-
- """
- 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 工作流监控
+
+
+
+
+
+
+
+
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.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 += `
+
+ `;
+ });
+}
+
+// 页面加载时自动刷新数据
+document.addEventListener('DOMContentLoaded', refreshData);
+
+// 每30秒自动刷新一次
+setInterval(refreshData, 30000);