diff --git a/static/index.html b/static/index.html
index a63dd9de..fc5709a0 100644
--- a/static/index.html
+++ b/static/index.html
@@ -1526,7 +1526,7 @@ async function renderHealth() {
html += '
';
html += '
📩 入站
' + (ba.inbound || 0) + '
';
@@ -1536,6 +1536,19 @@ async function renderHealth() {
if (ba.last_error) {
html += '
' + ba.last_error + '
';
}
+
+ // Hermes Gateway + LLM Provider
+ const gw = xmpp.gateways || {};
+ const llm = xmpp.llm_provider || {};
+ html += '
';
+ html += '
🔌 Hermes Gateway
';
+ Object.entries(gw).forEach(([k,v]) => {
+ html += '' + k + ':' + v.port + (v.alive ? ' ✅' : ' ❌') + ' ';
+ });
+ html += '
';
+ html += '
🧠 LLM Provider
' + (llm.status || '?') + '';
+ if (llm.error) html += '
' + llm.error + '';
+ html += '
';
html += '
';
}
el.innerHTML = html;
@@ -1578,7 +1591,7 @@ async function refreshHealth() {
}
});
- // 更新 XMPP 详情
+ // 更新 XMPP 详情 + Bot 活动
if (xmpp) {
const d_age = document.getElementById('hXmppDetail_age');
const d_err = document.getElementById('hXmppDetail_err');
@@ -1588,22 +1601,24 @@ async function refreshHealth() {
if (d_err) d_err.textContent = xmpp.error_rate_1h + '%';
if (d_eja) d_eja.textContent = xmpp.ejabberd || '?';
if (d_st) d_st.textContent = xmpp.status;
- // 更新卡片颜色
if (ageEl && xmpp) ageEl.style.color = xmpp.last_message_age_sec > 600 ? '#f85149' : '#3fb950';
if (labelEl && xmpp) {
const labels = {ok:'XMPP 正常', degraded:'XMPP 降级', critical:'XMPP 断联'};
labelEl.textContent = labels[xmpp.status] || 'XMPP 无数据';
}
- // 更新 Bot 活动
+ // Bot 活动
const ba = xmpp.bot_activity || {};
- const bi = document.getElementById('hBot_in');
- const bo = document.getElementById('hBot_out');
- const be = document.getElementById('hBot_err');
- const bl = document.getElementById('hBot_lastErr');
+ const bi = document.getElementById('hBot_in'), bo = document.getElementById('hBot_out');
+ const be = document.getElementById('hBot_err'), bl = document.getElementById('hBot_lastErr');
if (bi) bi.textContent = ba.inbound || 0;
if (bo) { bo.textContent = ba.outbound || 0; bo.className = 'font-mono ' + (ba.outbound ? 'text-[#3fb950]' : (ba.inbound ? 'text-[#f85149]' : 'text-slate-500')); }
if (be) { be.textContent = ba.errors || 0; be.className = 'font-mono ' + (ba.errors ? 'text-[#f85149]' : 'text-slate-500'); }
- if (bl && ba.last_error) bl.textContent = ba.last_error;
+ if (bl) bl.textContent = ba.last_error || '';
+ // LLM Provider
+ const llm = xmpp.llm_provider || {};
+ const ls = document.getElementById('hLlmStatus'), le = document.getElementById('hLlmErr');
+ if (ls) { ls.textContent = llm.status || '?'; ls.className = 'font-mono ' + (llm.status === 'ok' ? 'text-[#3fb950]' : 'text-[#f85149]'); }
+ if (le) le.textContent = llm.error || '';
}
// 更新时间戳
diff --git a/xmpp_logger.py b/xmpp_logger.py
index 9d4c9e07..aadc1bb8 100644
--- a/xmpp_logger.py
+++ b/xmpp_logger.py
@@ -142,78 +142,91 @@ def stats():
def health():
- """快速健康检查:返回最后消息年龄 + 最近1h错误率 + bot 日志状态"""
- result = {"status": "no_data", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {}}
+ """快速健康检查:返回最后消息年龄 + 最近1h错误率 + bot 日志状态 + Hermes Gateway 状态"""
+ result = {"status": "no_data", "last_message_age_sec": -1, "error_rate_1h": 0,
+ "bot_activity": {}, "gateways": {}, "llm_provider": {}}
# 1. 检查 xmpp_messages.jsonl
+ now_epoch = _time.time()
if LOG_FILE.exists():
- now_epoch = _time.time()
last_epoch = 0
recent_total = 0
recent_errors = 0
one_hour_ago = now_epoch - 3600
-
try:
with open(LOG_FILE, "r", encoding="utf-8") as f:
for line in f:
try:
e = json.loads(line)
ep = e.get("epoch", 0)
- if ep > last_epoch:
- last_epoch = ep
+ if ep > last_epoch: last_epoch = ep
if ep > one_hour_ago:
recent_total += 1
- if e["status"] != "ok":
- recent_errors += 1
- except Exception:
- continue
- except Exception:
- pass
+ if e["status"] != "ok": recent_errors += 1
+ except Exception: continue
+ except Exception: pass
+ result["last_message_age_sec"] = int(now_epoch - last_epoch) if last_epoch else -1
+ result["error_rate_1h"] = round(recent_errors / recent_total * 100, 1) if recent_total else 0
- last_age = int(now_epoch - last_epoch) if last_epoch else -1
- err_rate = round(recent_errors / recent_total * 100, 1) if recent_total else 0
- result["last_message_age_sec"] = last_age
- result["error_rate_1h"] = err_rate
-
- # 2. 检查知微 Bot systemd journal(最近5分钟)
+ # 2. 检查知微 Bot systemd journal + 持久化状态
try:
import subprocess
- r = subprocess.run(
- ["journalctl", "-u", "xmpp-zhiwei", "--no-pager", "--since", "5 min ago", "-o", "cat"],
- capture_output=True, timeout=5, text=True
- )
+ r = subprocess.run(["journalctl", "-u", "xmpp-zhiwei", "--no-pager", "--since", "30 min ago", "-o", "cat"],
+ capture_output=True, timeout=5, text=True)
lines = [l for l in r.stdout.split("\n") if l.strip()]
inbound = [l for l in lines if "📩 收到" in l]
outbound = [l for l in lines if "📤 发送" in l or "📤" in l]
- errors = [l for l in lines if "ERROR" in l or "error" in l or "timeout" in l or "Timeout" in l]
+ errors = [l for l in lines if "ERROR" in l or "TimeoutError" in l or "timed out" in l]
result["bot_activity"] = {
- "inbound": len(inbound),
- "outbound": len(outbound),
- "errors": len(errors),
- "last_error": errors[-1][:200] if errors else None,
- "last_inbound": inbound[-1][:200] if inbound else None,
+ "inbound": len(inbound), "outbound": len(outbound), "errors": len(errors),
+ "last_error": errors[-1][:250] if errors else None,
+ "last_inbound": inbound[-1][:250] if inbound else None,
+ "last_outbound": outbound[-1][:200] if outbound else None,
}
- # 有错误且无出站 → degraded
- if errors and not outbound:
- if result.get("status") != "critical":
- result["status"] = "degraded"
- # 有入站无出站 → 可能卡住了
- if inbound and not outbound:
- result["status"] = "degraded"
+ if errors and not outbound: result["status"] = "degraded"
+ if inbound and not outbound: result["status"] = "degraded"
except Exception:
result["bot_activity"] = {"error": "journalctl failed"}
- # 3. 综合判定
+ # 3. 检查 Hermes Gateway 各 profile 状态(端口检测)
+ try:
+ import socket
+ profiles = {"zhiwei": 8643, "mohe": 8642, "xiaoguo": 8645}
+ for name, port in profiles.items():
+ ok = False
+ try:
+ s = socket.create_connection(("127.0.0.1", port), timeout=2)
+ s.close()
+ ok = True
+ except Exception: pass
+ result["gateways"][name] = {"port": port, "alive": ok}
+
+ # 测试知微的 LLM 调用是否可达
+ if result["gateways"].get("zhiwei", {}).get("alive"):
+ try:
+ import urllib.request
+ payload = json.dumps({"model": "gpt-4", "messages": [{"role": "user", "content": "ping"}],
+ "max_tokens": 5, "stream": False}).encode()
+ req = urllib.request.Request("http://127.0.0.1:8643/v1/chat/completions",
+ data=payload, headers={"Content-Type": "application/json",
+ "Authorization": "Bearer hermes123"})
+ urllib.request.urlopen(req, timeout=8)
+ result["llm_provider"] = {"status": "ok", "latency": "fast"}
+ except Exception as e:
+ result["llm_provider"] = {"status": "timeout" if "timeout" in str(e).lower() else "error",
+ "error": str(e)[:150]}
+ except Exception as e:
+ result["gateways"] = {"error": str(e)[:100]}
+
+ # 4. 综合判定
if result.get("status") != "degraded":
la = result.get("last_message_age_sec", -1)
er = result.get("error_rate_1h", 0)
- if la < 0:
- result["status"] = "no_data"
- elif la > 600:
- result["status"] = "critical"
- elif er > 50:
- result["status"] = "degraded"
- elif la > 0:
- result["status"] = "ok"
+ llm = result.get("llm_provider", {}).get("status", "")
+ if llm in ("timeout", "error"): result["status"] = "degraded"
+ elif la < 0: result["status"] = "no_data"
+ elif la > 600: result["status"] = "critical"
+ elif er > 50: result["status"] = "degraded"
+ elif la > 0: result["status"] = "ok"
return result