feat: add monitoring feature with backend API and frontend UI for settings.

This commit is contained in:
2026-01-30 11:34:06 +08:00
parent 55a0e46487
commit 59441adc99
3 changed files with 185 additions and 7 deletions

View File

@@ -114,6 +114,10 @@ func startServer(cfg *config.Config) {
mux.HandleFunc("/api/team/status", api.CORS(api.HandleTeamProcessStatus)) mux.HandleFunc("/api/team/status", api.CORS(api.HandleTeamProcessStatus))
mux.HandleFunc("/api/team/stop", api.CORS(api.HandleTeamProcessStop)) mux.HandleFunc("/api/team/stop", api.CORS(api.HandleTeamProcessStop))
// 监控设置 API
mux.HandleFunc("/api/monitor/settings", api.CORS(api.HandleGetMonitorSettings))
mux.HandleFunc("/api/monitor/settings/save", api.CORS(api.HandleSaveMonitorSettings))
// 嵌入的前端静态文件 // 嵌入的前端静态文件
if web.IsEmbedded() { if web.IsEmbedded() {
webFS := web.GetFileSystem() webFS := web.GetFileSystem()

View File

@@ -0,0 +1,94 @@
package api
import (
"encoding/json"
"net/http"
"strconv"
"codex-pool/internal/database"
)
// MonitorSettings 监控设置
type MonitorSettings struct {
Target int `json:"target"`
AutoAdd bool `json:"auto_add"`
MinInterval int `json:"min_interval"`
PollingEnabled bool `json:"polling_enabled"`
PollingInterval int `json:"polling_interval"`
}
// HandleGetMonitorSettings 获取监控设置
func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
Error(w, http.StatusMethodNotAllowed, "仅支持 GET")
return
}
if database.Instance == nil {
Error(w, http.StatusInternalServerError, "数据库未初始化")
return
}
settings := MonitorSettings{
Target: 50,
AutoAdd: false,
MinInterval: 300,
PollingEnabled: false,
PollingInterval: 60,
}
if val, _ := database.Instance.GetConfig("monitor_target"); val != "" {
if v, err := strconv.Atoi(val); err == nil {
settings.Target = v
}
}
if val, _ := database.Instance.GetConfig("monitor_auto_add"); val == "true" {
settings.AutoAdd = true
}
if val, _ := database.Instance.GetConfig("monitor_min_interval"); val != "" {
if v, err := strconv.Atoi(val); err == nil {
settings.MinInterval = v
}
}
if val, _ := database.Instance.GetConfig("monitor_polling_enabled"); val == "true" {
settings.PollingEnabled = true
}
if val, _ := database.Instance.GetConfig("monitor_polling_interval"); val != "" {
if v, err := strconv.Atoi(val); err == nil {
settings.PollingInterval = v
}
}
Success(w, settings)
}
// HandleSaveMonitorSettings 保存监控设置
func HandleSaveMonitorSettings(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
Error(w, http.StatusMethodNotAllowed, "仅支持 POST")
return
}
if database.Instance == nil {
Error(w, http.StatusInternalServerError, "数据库未初始化")
return
}
var settings MonitorSettings
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
Error(w, http.StatusBadRequest, "解析请求失败")
return
}
// 保存到数据库
database.Instance.SetConfig("monitor_target", strconv.Itoa(settings.Target))
database.Instance.SetConfig("monitor_auto_add", strconv.FormatBool(settings.AutoAdd))
database.Instance.SetConfig("monitor_min_interval", strconv.Itoa(settings.MinInterval))
database.Instance.SetConfig("monitor_polling_enabled", strconv.FormatBool(settings.PollingEnabled))
database.Instance.SetConfig("monitor_polling_interval", strconv.Itoa(settings.PollingInterval))
Success(w, map[string]interface{}{
"message": "设置已保存",
"settings": settings,
})
}

View File

@@ -58,13 +58,16 @@ export default function Monitor() {
const [checkingHealth, setCheckingHealth] = useState(false) const [checkingHealth, setCheckingHealth] = useState(false)
const [autoPauseEnabled, setAutoPauseEnabled] = useState(false) const [autoPauseEnabled, setAutoPauseEnabled] = useState(false)
// 配置表单状态 // 配置表单状态 - 从后端 SQLite 加载
const [targetInput, setTargetInput] = useState(50) const [targetInput, setTargetInput] = useState(50)
const [autoAdd, setAutoAdd] = useState(false) const [autoAdd, setAutoAdd] = useState(false)
const [minInterval, setMinInterval] = useState(300) const [minInterval, setMinInterval] = useState(300)
const [pollingEnabled, setPollingEnabled] = useState(false) const [pollingEnabled, setPollingEnabled] = useState(false)
const [pollingInterval, setPollingInterval] = useState(60) const [pollingInterval, setPollingInterval] = useState(60)
// 倒计时状态
const [countdown, setCountdown] = useState(60)
// 使用后端 S2A 代理访问 S2A 服务器 // 使用后端 S2A 代理访问 S2A 服务器
const proxyBase = '/api/s2a/proxy' const proxyBase = '/api/s2a/proxy'
@@ -128,10 +131,29 @@ export default function Monitor() {
setRefreshing(false) setRefreshing(false)
}, [targetInput, autoAdd, minInterval, pollingEnabled, pollingInterval]) }, [targetInput, autoAdd, minInterval, pollingEnabled, pollingInterval])
// 设置目标 - 本地状态管理S2A 没有此 API // 设置目标 - 保存到后端 SQLite
const handleSetTarget = async () => { const handleSetTarget = async () => {
setLoading(true) setLoading(true)
// 由于 S2A 没有 pool/target API这里只更新本地状态 try {
// 保存到后端数据库
const res = await fetch('/api/monitor/settings/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
target: targetInput,
auto_add: autoAdd,
min_interval: minInterval,
polling_enabled: pollingEnabled,
polling_interval: pollingInterval,
}),
})
if (!res.ok) {
console.error('保存设置失败:', res.status)
}
} catch (e) {
console.error('保存设置失败:', e)
}
// 更新本地状态
setPoolStatus(prev => prev ? { setPoolStatus(prev => prev ? {
...prev, ...prev,
target: targetInput, target: targetInput,
@@ -144,13 +166,29 @@ export default function Monitor() {
setLoading(false) setLoading(false)
} }
// 控制轮询 - 本地状态管理 // 控制轮询 - 保存到后端 SQLite
const handleTogglePolling = async () => { const handleTogglePolling = async () => {
setLoading(true) setLoading(true)
setPollingEnabled(!pollingEnabled) const newPollingEnabled = !pollingEnabled
setPollingEnabled(newPollingEnabled)
try {
await fetch('/api/monitor/settings/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
target: targetInput,
auto_add: autoAdd,
min_interval: minInterval,
polling_enabled: newPollingEnabled,
polling_interval: pollingInterval,
}),
})
} catch (e) {
console.error('保存轮询设置失败:', e)
}
setPoolStatus(prev => prev ? { setPoolStatus(prev => prev ? {
...prev, ...prev,
polling_enabled: !pollingEnabled, polling_enabled: newPollingEnabled,
polling_interval: pollingInterval, polling_interval: pollingInterval,
} : null) } : null)
setLoading(false) setLoading(false)
@@ -186,13 +224,55 @@ export default function Monitor() {
setAutoAddLogs([]) setAutoAddLogs([])
} }
// 从后端加载监控设置
const loadMonitorSettings = async () => {
try {
const res = await fetch('/api/monitor/settings')
if (res.ok) {
const json = await res.json()
if (json.code === 0 && json.data) {
const s = json.data
setTargetInput(s.target || 50)
setAutoAdd(s.auto_add || false)
setMinInterval(s.min_interval || 300)
setPollingEnabled(s.polling_enabled || false)
setPollingInterval(s.polling_interval || 60)
}
}
} catch (e) {
console.error('加载监控设置失败:', e)
}
}
// 初始化 // 初始化
useEffect(() => { useEffect(() => {
loadMonitorSettings()
fetchPoolStatus() fetchPoolStatus()
refreshStats() refreshStats()
fetchAutoAddLogs() fetchAutoAddLogs()
}, [fetchPoolStatus, refreshStats]) }, [fetchPoolStatus, refreshStats])
// 倒计时定时器 - 当启用轮询时
useEffect(() => {
// 初始化倒计时
setCountdown(pollingInterval)
if (!pollingEnabled) return
const timer = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
// 倒计时结束,刷新数据并重置
refreshStats()
return pollingInterval
}
return prev - 1
})
}, 1000)
return () => clearInterval(timer)
}, [pollingEnabled, pollingInterval, refreshStats])
// 计算健康状态 // 计算健康状态
const healthySummary = healthResults.reduce( const healthySummary = healthResults.reduce(
(acc, r) => { (acc, r) => {
@@ -309,7 +389,7 @@ export default function Monitor() {
{pollingEnabled && ( {pollingEnabled && (
<p className="mt-2 text-xs text-slate-500 flex items-center gap-1"> <p className="mt-2 text-xs text-slate-500 flex items-center gap-1">
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3" />
{pollingInterval} <span className="font-mono text-green-500">{countdown}s</span> ( {pollingInterval} )
</p> </p>
)} )}
</CardContent> </CardContent>