From 59441adc99f53040afac9b658adb0b2ca69176ee Mon Sep 17 00:00:00 2001 From: kyx236 Date: Fri, 30 Jan 2026 11:34:06 +0800 Subject: [PATCH] feat: add monitoring feature with backend API and frontend UI for settings. --- backend/cmd/main.go | 4 ++ backend/internal/api/monitor.go | 94 +++++++++++++++++++++++++++++++++ frontend/src/pages/Monitor.tsx | 94 ++++++++++++++++++++++++++++++--- 3 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 backend/internal/api/monitor.go diff --git a/backend/cmd/main.go b/backend/cmd/main.go index a1e10ff..162ceea 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -114,6 +114,10 @@ func startServer(cfg *config.Config) { mux.HandleFunc("/api/team/status", api.CORS(api.HandleTeamProcessStatus)) 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() { webFS := web.GetFileSystem() diff --git a/backend/internal/api/monitor.go b/backend/internal/api/monitor.go new file mode 100644 index 0000000..de8d138 --- /dev/null +++ b/backend/internal/api/monitor.go @@ -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, + }) +} diff --git a/frontend/src/pages/Monitor.tsx b/frontend/src/pages/Monitor.tsx index 14a862a..12ddbe1 100644 --- a/frontend/src/pages/Monitor.tsx +++ b/frontend/src/pages/Monitor.tsx @@ -58,13 +58,16 @@ export default function Monitor() { const [checkingHealth, setCheckingHealth] = useState(false) const [autoPauseEnabled, setAutoPauseEnabled] = useState(false) - // 配置表单状态 + // 配置表单状态 - 从后端 SQLite 加载 const [targetInput, setTargetInput] = useState(50) const [autoAdd, setAutoAdd] = useState(false) const [minInterval, setMinInterval] = useState(300) const [pollingEnabled, setPollingEnabled] = useState(false) const [pollingInterval, setPollingInterval] = useState(60) + // 倒计时状态 + const [countdown, setCountdown] = useState(60) + // 使用后端 S2A 代理访问 S2A 服务器 const proxyBase = '/api/s2a/proxy' @@ -128,10 +131,29 @@ export default function Monitor() { setRefreshing(false) }, [targetInput, autoAdd, minInterval, pollingEnabled, pollingInterval]) - // 设置目标 - 本地状态管理(S2A 没有此 API) + // 设置目标 - 保存到后端 SQLite const handleSetTarget = async () => { 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 ? { ...prev, target: targetInput, @@ -144,13 +166,29 @@ export default function Monitor() { setLoading(false) } - // 控制轮询 - 本地状态管理 + // 控制轮询 - 保存到后端 SQLite const handleTogglePolling = async () => { 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 ? { ...prev, - polling_enabled: !pollingEnabled, + polling_enabled: newPollingEnabled, polling_interval: pollingInterval, } : null) setLoading(false) @@ -186,13 +224,55 @@ export default function Monitor() { 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(() => { + loadMonitorSettings() fetchPoolStatus() refreshStats() fetchAutoAddLogs() }, [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( (acc, r) => { @@ -309,7 +389,7 @@ export default function Monitor() { {pollingEnabled && (

- 每 {pollingInterval} 秒刷新 + {countdown}s 后刷新 (每 {pollingInterval} 秒)

)}