| """MoltBot 状态监控网页""" |
|
|
| import json |
| import os |
| import subprocess |
| import time |
| from datetime import datetime, timedelta |
|
|
| import psutil |
| from flask import Flask, Response |
|
|
| app = Flask(__name__) |
| START_TIME = time.time() |
|
|
|
|
| def get_gateway_status(): |
| """检查 OpenClaw Gateway 是否在运行""" |
| for proc in psutil.process_iter(["pid", "name", "cmdline"]): |
| try: |
| cmdline = " ".join(proc.info.get("cmdline") or []) |
| if "openclaw" in cmdline and "gateway" in cmdline: |
| return {"running": True, "pid": proc.info["pid"]} |
| except (psutil.NoSuchProcess, psutil.AccessDenied): |
| continue |
| return {"running": False, "pid": None} |
|
|
|
|
| def get_feishu_status(): |
| """检查飞书连接状态(通过日志)""" |
| log_file = f"/tmp/openclaw/openclaw-{datetime.now().strftime('%Y-%m-%d')}.log" |
| if not os.path.exists(log_file): |
| return "unknown" |
| try: |
| result = subprocess.run( |
| ["tail", "-100", log_file], |
| capture_output=True, text=True, timeout=5 |
| ) |
| lines = result.stdout |
| if "[feishu]" in lines: |
| if "ws client ready" in lines or "Received from" in lines: |
| return "connected" |
| elif "auto-restart" in lines: |
| return "reconnecting" |
| return "initializing" |
| except Exception: |
| return "unknown" |
|
|
|
|
| def get_message_count(): |
| """统计今日消息数""" |
| log_file = f"/tmp/openclaw/openclaw-{datetime.now().strftime('%Y-%m-%d')}.log" |
| if not os.path.exists(log_file): |
| return 0 |
| try: |
| result = subprocess.run( |
| ["grep", "-c", "Received from", log_file], |
| capture_output=True, text=True, timeout=5 |
| ) |
| return int(result.stdout.strip() or 0) |
| except Exception: |
| return 0 |
|
|
|
|
| def get_system_info(): |
| """获取系统资源信息""" |
| uptime = timedelta(seconds=int(time.time() - START_TIME)) |
| return { |
| "cpu_percent": psutil.cpu_percent(interval=0.5), |
| "memory": psutil.virtual_memory()._asdict(), |
| "uptime": str(uptime), |
| } |
|
|
|
|
| def format_bytes(b): |
| """格式化字节""" |
| for unit in ["B", "KB", "MB", "GB"]: |
| if b < 1024: |
| return f"{b:.1f} {unit}" |
| b /= 1024 |
| return f"{b:.1f} TB" |
|
|
|
|
| STATUS_COLORS = { |
| "connected": "#00e676", |
| "reconnecting": "#ffc107", |
| "initializing": "#2196f3", |
| "unknown": "#9e9e9e", |
| } |
|
|
| STATUS_TEXT = { |
| "connected": "✅ 已连接", |
| "reconnecting": "🔄 重连中", |
| "initializing": "⏳ 初始化中", |
| "unknown": "❓ 未知", |
| } |
|
|
|
|
| @app.route("/") |
| def index(): |
| gw = get_gateway_status() |
| feishu = get_feishu_status() |
| msgs = get_message_count() |
| sys_info = get_system_info() |
| mem = sys_info["memory"] |
|
|
| feishu_color = STATUS_COLORS.get(feishu, "#9e9e9e") |
| feishu_text = STATUS_TEXT.get(feishu, "❓ 未知") |
| gw_color = "#00e676" if gw["running"] else "#f44336" |
| gw_text = f"✅ 运行中 (PID {gw['pid']})" if gw["running"] else "❌ 已停止" |
|
|
| html = f"""<!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <meta http-equiv="refresh" content="30"> |
| <title>MoltBot 状态监控</title> |
| <style> |
| * {{ margin: 0; padding: 0; box-sizing: border-box; }} |
| body {{ |
| font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; |
| background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); |
| min-height: 100vh; |
| color: #e0e0e0; |
| padding: 20px; |
| }} |
| .container {{ max-width: 800px; margin: 0 auto; }} |
| .header {{ |
| text-align: center; |
| padding: 40px 20px 30px; |
| }} |
| .header h1 {{ |
| font-size: 2.2em; |
| background: linear-gradient(135deg, #667eea, #764ba2); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| margin-bottom: 8px; |
| }} |
| .header p {{ color: #9e9e9e; font-size: 0.95em; }} |
| .cards {{ display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 24px; }} |
| .card {{ |
| background: rgba(255,255,255,0.06); |
| backdrop-filter: blur(12px); |
| border: 1px solid rgba(255,255,255,0.08); |
| border-radius: 16px; |
| padding: 24px; |
| transition: transform 0.2s, box-shadow 0.2s; |
| }} |
| .card:hover {{ |
| transform: translateY(-2px); |
| box-shadow: 0 8px 32px rgba(102,126,234,0.15); |
| }} |
| .card-title {{ |
| font-size: 0.85em; |
| color: #9e9e9e; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| margin-bottom: 12px; |
| }} |
| .card-value {{ |
| font-size: 1.4em; |
| font-weight: 600; |
| }} |
| .status-dot {{ |
| display: inline-block; |
| width: 10px; height: 10px; |
| border-radius: 50%; |
| margin-right: 8px; |
| animation: pulse 2s infinite; |
| }} |
| @keyframes pulse {{ |
| 0%, 100% {{ opacity: 1; }} |
| 50% {{ opacity: 0.5; }} |
| }} |
| .card-full {{ grid-column: 1 / -1; }} |
| .stats-grid {{ display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 12px; }} |
| .stat {{ text-align: center; }} |
| .stat-value {{ font-size: 1.6em; font-weight: 700; color: #667eea; }} |
| .stat-label {{ font-size: 0.8em; color: #9e9e9e; margin-top: 4px; }} |
| .progress-bar {{ |
| width: 100%; |
| height: 8px; |
| background: rgba(255,255,255,0.1); |
| border-radius: 4px; |
| margin-top: 8px; |
| overflow: hidden; |
| }} |
| .progress-fill {{ |
| height: 100%; |
| border-radius: 4px; |
| transition: width 0.5s; |
| }} |
| .footer {{ |
| text-align: center; |
| padding: 30px; |
| color: #616161; |
| font-size: 0.85em; |
| }} |
| @media (max-width: 600px) {{ |
| .cards {{ grid-template-columns: 1fr; }} |
| .stats-grid {{ grid-template-columns: 1fr; }} |
| }} |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="header"> |
| <h1>🤖 MoltBot AI</h1> |
| <p>飞书智能助手 · 状态监控</p> |
| </div> |
| |
| <div class="cards"> |
| <div class="card"> |
| <div class="card-title">OpenClaw 网关</div> |
| <div class="card-value"> |
| <span class="status-dot" style="background:{gw_color}"></span> |
| {gw_text} |
| </div> |
| </div> |
| |
| <div class="card"> |
| <div class="card-title">飞书连接</div> |
| <div class="card-value"> |
| <span class="status-dot" style="background:{feishu_color}"></span> |
| {feishu_text} |
| </div> |
| </div> |
| |
| <div class="card card-full"> |
| <div class="card-title">系统概览</div> |
| <div class="stats-grid"> |
| <div class="stat"> |
| <div class="stat-value">{sys_info['uptime']}</div> |
| <div class="stat-label">运行时长</div> |
| </div> |
| <div class="stat"> |
| <div class="stat-value">{msgs}</div> |
| <div class="stat-label">今日消息</div> |
| </div> |
| <div class="stat"> |
| <div class="stat-value">{os.environ.get('MODEL_NAME', 'N/A')}</div> |
| <div class="stat-label">当前模型</div> |
| </div> |
| </div> |
| </div> |
| |
| <div class="card"> |
| <div class="card-title">CPU 使用率</div> |
| <div class="card-value">{sys_info['cpu_percent']}%</div> |
| <div class="progress-bar"> |
| <div class="progress-fill" style="width:{sys_info['cpu_percent']}%;background:linear-gradient(90deg,#667eea,#764ba2)"></div> |
| </div> |
| </div> |
| |
| <div class="card"> |
| <div class="card-title">内存使用</div> |
| <div class="card-value">{format_bytes(mem['used'])} / {format_bytes(mem['total'])}</div> |
| <div class="progress-bar"> |
| <div class="progress-fill" style="width:{mem['percent']}%;background:linear-gradient(90deg,#00e676,#00c853)"></div> |
| </div> |
| </div> |
| </div> |
| |
| <div class="footer"> |
| 最后更新: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} · 自动刷新 30s |
| </div> |
| </div> |
| </body> |
| </html>""" |
| return Response(html, content_type="text/html; charset=utf-8") |
|
|
|
|
| @app.route("/health") |
| def health(): |
| gw = get_gateway_status() |
| return json.dumps({ |
| "status": "ok" if gw["running"] else "error", |
| "gateway": gw, |
| "feishu": get_feishu_status(), |
| "uptime": time.time() - START_TIME, |
| }), 200 if gw["running"] else 503 |
|
|
|
|
| if __name__ == "__main__": |
| app.run(host="0.0.0.0", port=7860) |
|
|