Files
hunjing/mcp/web-search/internal/mcp/monitor.go
T
2026-08-21 16:00:13 +08:00

243 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mcp
import (
"fmt"
"net/http"
"time"
"github.com/chenxin/hunjing/mcp/web-search/internal/models"
)
// HandleMonitor 返回 HTML 监控面板(GET /)。
func (h *Handler) HandleMonitor(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
uptime := time.Since(startTime).Round(time.Second).String()
// Provider 表格行
var providerRows string
for _, p := range h.providerKinds {
counts := h.balancer.ActiveKeyCount()
activeKeys := counts[p]
enabled := h.balancer.IsProviderEnabled(p)
statusIcon := "🟢"
statusText := "启用"
if !enabled {
statusIcon = "🔴"
statusText = "禁用"
}
providerRows += fmt.Sprintf(`<tr>
<td>%s</td>
<td><span class="badge badge-provider">%s</span></td>
<td>%s <span class="status-text">%s</span></td>
<td class="num">%d</td>
</tr>`, statusIcon, p, statusIcon, statusText, activeKeys)
}
// 缓存统计
l1Hits, l2Hits, misses := h.cache.Stats()
hitRate := h.cache.HitRate()
html := fmt.Sprintf(`<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>博闻 · 监控面板</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans SC", sans-serif;
background: #0f172a;
color: #e2e8f0;
padding: 24px;
line-height: 1.6;
}
.container { max-width: 960px; margin: 0 auto; }
h1 {
font-size: 24px;
font-weight: 600;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 12px;
}
h1 small {
font-size: 14px;
font-weight: 400;
color: #64748b;
}
.subtitle {
color: #64748b;
font-size: 14px;
margin-bottom: 24px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.card {
background: #1e293b;
border-radius: 12px;
padding: 16px 20px;
border: 1px solid #334155;
}
.card .label {
font-size: 12px;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 4px;
}
.card .value {
font-size: 28px;
font-weight: 700;
color: #38bdf8;
}
.card .value.green { color: #4ade80; }
.card .value.yellow { color: #fbbf24; }
.card .value.red { color: #f87171; }
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
color: #94a3b8;
}
table {
width: 100%%;
border-collapse: collapse;
background: #1e293b;
border-radius: 12px;
overflow: hidden;
border: 1px solid #334155;
margin-bottom: 24px;
}
th {
text-align: left;
padding: 12px 16px;
font-size: 12px;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
background: #1a2332;
border-bottom: 1px solid #334155;
}
td {
padding: 10px 16px;
border-bottom: 1px solid #1e293b;
font-size: 14px;
}
tr:last-child td { border-bottom: none; }
tr:hover { background: #243348; }
.num { text-align: right; font-variant-numeric: tabular-nums; }
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.badge-provider {
background: #1e3a5f;
color: #93c5fd;
}
.status-text {
font-size: 13px;
color: #94a3b8;
margin-left: 4px;
}
a { color: #38bdf8; text-decoration: none; }
a:hover { text-decoration: underline; }
.links { margin-top: 16px; display: flex; gap: 16px; font-size: 14px; }
.footer { margin-top: 32px; padding-top: 16px; border-top: 1px solid #334155; color: #475569; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<h1>🔍 博闻 <small>Bowen Web Search</small></h1>
<div class="subtitle">运行时间 %s</div>
<div class="grid">
<div class="card">
<div class="label">Provider</div>
<div class="value">%d</div>
</div>
<div class="card">
<div class="label">活跃密钥</div>
<div class="value">%d</div>
</div>
<div class="card">
<div class="label">缓存命中率</div>
<div class="value %s">%d%%%%</div>
</div>
<div class="card">
<div class="label">缓存条目</div>
<div class="value">%d</div>
</div>
</div>
<div class="section-title">📡 Provider 状态</div>
<table>
<thead>
<tr><th>状态</th><th>名称</th><th>开关</th><th class="num">活跃密钥</th></tr>
</thead>
<tbody>%s</tbody>
</table>
<div class="section-title">💾 缓存统计</div>
<table>
<thead>
<tr><th>指标</th><th class="num">值</th></tr>
</thead>
<tbody>
<tr><td>L1 命中(内存 LRU)</td><td class="num">%d</td></tr>
<tr><td>L2 命中(Redis)</td><td class="num">%d</td></tr>
<tr><td>未命中</td><td class="num">%d</td></tr>
<tr><td>总命中率</td><td class="num">%.1f%%%%</td></tr>
</tbody>
</table>
<div class="links">
<span>🔗</span>
<a href="/status">/status (JSON)</a>
<a href="/admin/keys">/admin/keys</a>
<a href="/health">/health</a>
</div>
<div class="footer">博闻 v2.0.0 · 数据实时 · 页面每 30 秒刷新</div>
</div>
<script>setTimeout(function(){ location.reload(); }, 30000);</script>
</body>
</html>`,
uptime,
len(h.providerKinds),
sumActiveKeys(h.balancer.ActiveKeyCount()),
hitRateColor(hitRate),
int(hitRate*100),
l1Hits+l2Hits+misses,
providerRows,
l1Hits, l2Hits, misses, hitRate*100,
)
fmt.Fprint(w, html)
}
func sumActiveKeys(m map[models.ProviderKind]int) int {
s := 0
for _, v := range m {
s += v
}
return s
}
func hitRateColor(rate float64) string {
if rate >= 0.6 {
return "green"
} else if rate >= 0.3 {
return "yellow"
}
return "red"
}