243 lines
5.8 KiB
Go
243 lines
5.8 KiB
Go
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"
|
||
}
|